Options
All
  • Public
  • Public/Protected
  • All
Menu

Module contract

Contract is an important concept of Flux, we provide here an introduction about the motivations and the benefits. Still, this may not be a must read for a first pass over the documentation (contract are optional anyway).

In Flux there is no limitation regarding the connection between modules: any module's output can be connected to any other module's input.

🤔 It would make no sense to introduce such restriction, in particular because small adaptors are often introduced to dynamically adapt messages reaching a module.

It follows that the data part of a Message reaching a module's input is literally unknown.

🧐 The following discussion address the validation of (only) the message's data; a similar discussion regarding configuration validation is provided elsewhere.

Contracts are introduced to both:

  • overcome the difficulty of having to deal with these unknown data
  • enable flexibility in terms of accepted incoming messages.

The latter point is important to keep in mind when designing modules as it allows to decrease friction on modules connectivity (meaning decreasing the need of adaptors).

Contract formalize two mechanisms:

  • a mechanism to declare pre-conditions that need to be fulfilled before triggering the execution of a module's process
  • a mechanism to retrieve a normalized data-structure from the data part of the messages

More often than not, modules need similar validation/transformation schemes within variations. That's why the formalism provided to define contracts has been designed to be easily composable.

Illustrative example

Let's consider a module featuring one input triggering an addition between two numbers when some data comes in. A naive implementation would look like this:

export class Module extends ModuleFlux {

     constructor( params ){
         super(params) 

         let result$ = this.addOutput({id:'result'})

         this.addInput({
              id:'input',
              description: 'addition between 2 numbers',
              onTriggered: ({data}: {data:unknown}) 
                             =>  result$.next({data:data[0] + data[1]})
         })
     }
 } 

The problem with this implementation is that it only works if the data reaching the module is an array with at least 2 elements, and with a first and second element matching a number. In other cases the process execution will most likely result in an error with not much context for the consumer of the module to understand.

Using a contract we can start formalizing the input's expectation:

let numberConcept = expect({
     description: 'a straight Number', 
     when: (data) => typeof(data)=='number' 
})

export class Module extends ModuleFlux {

     constructor( params ){
         super(params) 

         let result$ = this.addOutput({id:'result'})

         this.addInput({
              id:'input',
              description: 'addition between 2 numbers',
              contract: expectCount({count:2, when:numberConcept}),
              onTriggered: ({data}: {data:[number, number]})
                                      =>  result$.next({data:data[0] + data[1]})
         })
     }
 } 

This updated version already provides 3 benefits : pre-conditions check, data normalization, failures reporting.

✅ Pre-conditions check

The above contract ensures that the addition will be triggered only if exactly 2 numbers are available in the input message. Here we use a custom numberConcept expect to formalize what is a number, it is composed with the expectCount function that gets fulfilled if the incoming data contains two elements for which numberConcept applies.

✅ Data normalization

In case of FulfilledExpectation, the triggered function gets automatically feeded by the normalized data in lieu of the raw unknown one. Normalized data have a unique and predictable structure the process implementation can rely on. In addition, because normalized data are typed, all the benefits of a strong type system applies (in place of unknown there is now [number, number]).

✅ Errors reporting

In case of RejectedExpectation, an ExpectationStatus is accessible (it is the returned value of IExpectation.resolve function) to better understand what was wrong with the data (was it an array? did it contained not enough number? too much?)

Illustration of errors reporting in the flux-builder web application.

We can still improve the case study by extending the definition of numberConcept: we would like to handle strings that are valid representation of numbers, and also permit objects with a value property compatible with a number (straight or as a string).

let straightNumber = expect({
     description: 'a number', 
     when: (data) => typeof(data)=='number' 
})
let stringNumber = expect({
     description: 'a number from string',
     when: (data) => typeof(data)=='string' && !isNan(data), 
     normalizeTo: (data: string) => parseFloat(data)
})
let permissiveNumber = expectAnyOf({
     description: 'a permissive (leaf) number', 
     when:[
         straightNumber, 
         stringNumber
     ]
})
let numberConcept =  expectAnyOf({
     description: 'a permissive number', 
     when:[ 
         permissiveNumber, 
         expectAttribute({name:'value', when: permissiveNumber})
     ]
})

export class Module extends ModuleFlux {

     constructor( params ){
         super(params) 

         let result$ = this.addOutput({id:'result'})
         this.addInput({
              id:'input',
              description: 'addition between 2 numbers',
              contract: expectCount({count:2, when:numberConcept}),
              onTriggered: ({data}: {data:[number, number]}) =>  result$.next({data:data[0] + data[1]})
         })
     }
 } 

With this new implementation, the module now accepts quite a large range of input data (e.g. ['1',2], [{value:1}, {value:'2'}] ), and will fail 'gracefully' otherwise. Despite the number of possible combinations in terms of accepted inputs, the triggered function still get a nice and unique [number, number] parameter 🤘.

The above implementation exposes small bricks of expectation that can be easily re-used elsewhere. There are already several kinds of expectations coming with the library, e.g. expectSome, expectSingle, expectCount, expectAttribute, expectAllOf, expectAnyOf ... Should you want to derive your own expectation class, you need to derive from IExpectation.

The library provide another layer above expectations to go a (last) step further.

Index

Functions

contract

  • contract(__namedParameters: { description: string; optionals?: {}; requireds: {} }): Contract
  • Companion creation function of Contract.

    Parameters

    • __namedParameters: { description: string; optionals?: {}; requireds: {} }
      • description: string

        expectation's description

      • Optional optionals?: {}

        set of optionals expectations provided as a mapping using a given name

      • requireds: {}

        set of required expectations provided as a mapping using a given name

    Returns Contract

    Contract

expect

  • expect<T>(__namedParameters: { description: string; normalizeTo?: (data: any) => T; when: ((inputData: any) => boolean) | IExpectation<any> }): BaseExpectation<T>
  • Generic expectation creator, companion creation function of BaseExpectation.

    Type parameters

    • T

    Parameters

    • __namedParameters: { description: string; normalizeTo?: (data: any) => T; when: ((inputData: any) => boolean) | IExpectation<any> }
      • description: string

        expectation's description

      • Optional normalizeTo?: (data: any) => T

        normalizing function for fulfilled case

          • (data: any): T
          • Parameters

            • data: any

            Returns T

      • when: ((inputData: any) => boolean) | IExpectation<any>

        either a condition that returns true if the expectation is fulfilled or an expectation

    Returns BaseExpectation<T>

    BaseExpectation that resolve eventually to a type T

expectAllOf

  • expectAllOf<T>(__namedParameters: { description: string; normalizeTo?: (accData: any[]) => T; when: IExpectation<T>[] }): AllOf<T>
  • Companion creation function of AllOf.

    Type parameters

    • T

      The type of FulfilledExpectation.value, i.e. the type of the expectation return's value (normalized value) when the expectation is fulfilled

    Parameters

    • __namedParameters: { description: string; normalizeTo?: (accData: any[]) => T; when: IExpectation<T>[] }
      • description: string

        description of the expectation

      • Optional normalizeTo?: (accData: any[]) => T

        normalizing function

          • (accData: any[]): T
          • Parameters

            • accData: any[]

            Returns T

      • when: IExpectation<T>[]

        array of children expectations

    Returns AllOf<T>

    AllOf expectation

expectAnyOf

  • expectAnyOf<T>(__namedParameters: { description: string; normalizeTo?: (data: any) => T; when: IExpectation<T>[] }): AnyOf<T>
  • Companion creation function of AnyOf.

    Type parameters

    • T

      The type of FulfilledExpectation.value, i.e. the type of the expectation return's value (normalized value) when the expectation is fulfilled

    Parameters

    • __namedParameters: { description: string; normalizeTo?: (data: any) => T; when: IExpectation<T>[] }
      • description: string

        description of the expectation

      • Optional normalizeTo?: (data: any) => T

        normalizing function

          • (data: any): T
          • Parameters

            • data: any

            Returns T

      • when: IExpectation<T>[]

        array of children expectations

    Returns AnyOf<T>

    AnyOf expectation

expectAttribute

expectCount

  • The function expectCount aims at ensuring that exactly count elements in some inputData are fulfilled with respect to a given expectation.

    The expectation get fulfilled if both:

    • (i) the inputData is an array
    • (ii) there exist exactly count elements in the array that verifies when

    In that case, the normalized data is an array containing the normalized data of the elements fulfilled. (of length count)

    Type parameters

    • T

    Parameters

    • __namedParameters: { count: number; when: IExpectation<T> }
      • count: number

        the expected count

      • when: IExpectation<T>

        the expectation

    Returns BaseExpectation<T[]>

    BaseExpectation that resolve eventually to a type T[] of length count

expectFree

expectInstanceOf

  • expectInstanceOf<T>(__namedParameters: { Type: any; attNames?: string[]; typeName: string }): BaseExpectation<T>
  • The function expectInstanceOf aims at ensuring that at least one element of target instance type exists in input data.

    The expectation get fulfilled if any of the following get fulfilled:

    • the inputData is an instance of Type
    • the inputData have an attribute in attNames that is an instance of Type

    In that case, the normalized data is the instance of Type retrieved.

    Type parameters

    • T

    Parameters

    • __namedParameters: { Type: any; attNames?: string[]; typeName: string }
      • Type: any

        the target type

      • Optional attNames?: string[]

        candidate attribute names

      • typeName: string

        display name of the type

    Returns BaseExpectation<T>

    BaseExpectation that resolve eventually to a type T

expectSingle

  • The function expectSingle aims at ensuring that exactly one element in some inputData is fulfilled with respect to a given expectation.

    The expectation get fulfilled if either:

    • (i) when.resolve(inputData) is fulfilled
    • (ii) inputData is an array that contains exactly one element for which when resolve to FulfilledExpectation

    In that case, the normalized data identifies to the one of the when expectation.

    Type parameters

    • T

    Parameters

    Returns BaseExpectation<T>

    BaseExpectation that resolve eventually to a type T[] of length count

expectSome

  • The function expectSome aims at ensuring that it exist at least one elements in some inputData that are fulfilled with respect to a given expectation.

    The expectation get fulfilled if either:

    • (i) when.resolve(inputData) is fulfilled
    • (ii) inputData is an array that contains at least one element that verifies when

    In that case, the normalized data is an array containing the normalized data of the elements fulfilled.

    Type parameters

    • T

    Parameters

    Returns BaseExpectation<T[]>

    BaseExpectation that resolve eventually to a type T[]

Let freeContract

Generated using TypeDoc