Hub Domain Specific Language (DSL)

The Hub DSL provides an implementation model for expressing digital onboarding solutions in a concise manner without superfluous details.

Hub Domain Specific Language (DSL)

The Hub DSL provides an implementation model for expressing digital onboarding solutions in a concise manner without superfluous details.
Letting developers focus on the business logic.

In addition to the main purpose Hub DSL supports these objectives:

  • enables transparent handling of failures,
  • facilitates working with data payloads, data mapping and transformation,
  • bridges the gap between developers and domain experts using a common language.

The Hub DSL provides the following features

DSL Commands

Route

A route represents an interaction with a user.

Typically, the goal is to display route-specific information and gather input from the user.
A route is rendered by a Hub Client as a web page or mobile app screen, depending on the Hub Client implementation.

A route is identified by its name and can be used in a workflow definition.
A minimal definition specifies a route uri intended for a Hub Client.

route('name') {
    uri '/uri'
}

Definition and usage

It is possible provide a route definition inline within a workflow definition.

worklfow('test') {
    route('name') {
        uri '/uri'
    }
}

Another option is to define a route as part of a Hub component and use it in a workflow by referencing the route name.
This approach facilitates route reusability and separation of concerns.

route('name') {
    uri '/uri'
}

worklfow('test') {
    route('name')
}

Additionally, it is possible to reference a route by name and provide additional details when used in a workflow.
This allows for separating a route definition (uri, data constrains) and usage (export, namespace, checkpoint).

route('client') {
    uri '/client'
    validate {
        firstname
        lastname
    }
}

worklfow('test') {
    route('client') {
        export documents
        namespace application.client
    }
}

Route Result

A route result is stored using a namespace attribute key.

route('client-info') {
  uri '/client-info'
  namespace client
}

If a validate block is specific, a route result is validated before storing the result and resuming the execution.
The route submit request results in a validation error if the validation fails.

route('client-info') {
    uri '/client-info'
    namespace client
    validate {
      firstname
      lastname
      idFront { file 'image'}
    }
}

Exporting data

In order to pass route data, the export is used. Any JSON-like data can be exported, using context attributes or serializable values.

products << [product1: "Product1", product2: "Product2"]
route('products') {
    uri '/products'
    export products
}

route('greeting') {
    uri '/greeting'
    export message: "Hello world!"
}

Route check-point

A route can be marked as a check-point, meaning it is disabled to go back to the previous route.

route('finish') {
    uri '/finish'
    checkpoint()
}

Terminal route

A terminal route marks the end of a workflow execution. The corresponding execution is terminated when a terminal route is executed.

Also, a terminal route is marked as a check-point.

route('finish') {
    uri '/finish'
    terminal()
}

In addition, it is possible to set an execution result payload using a terminal route.

route('finish') {
    uri '/finish'
    terminal(payload)
}

Route functions

A route function allows a Hub Client to execute functions in the context of the given route.
A Hub client executes a route function via Hub Client API.

Some use-cases of route functions:

  • dynamic queries based on user input, like auto-complete,
  • asynchronous data processing, like document OCR,
  • communication between different execution.
route('name') {
    uri '/uri'
    function('fnc1') {
      context initial
      namespace fnc1
    }
}
  • initial execution context for corresponding function execution
  • namespace stores the result of a route function execution

It is possible to specify one or more route functions.

See route examples for more details.

Exchange

An exchange is a connector proxy. It makes external (API) calls using an HTTP connector or a custom connector.

It provides the following tools for handling connector failures:

  • timeout, an exchange fails with an error if the connector does not respond within the specified timeout
  • retry strategy, retries failed connector requests
  • fallback, a workflow, function or expression that is executed when an exchange fails
  • validate, specifies a valid connector response, an exchange fails with an error if the connector response doesn't pass the validation

An exchange is executed asynchronously when marked with async().

HTTP connector

It is possible to use built-in HTTP connector to make external calls, see more details.

http {
  definition
}

Custom connector

Optionally, an exchange can use a custom connector with config.

exchange('name') {
  connector('custom')
  config input
}

Exchange Result

An exchange result is stored using a namespace attribute key.

exchange('localhost-api') {
  http {
    url "https://localhost:8080/api"
  }
  namespace api
}

Exchange Result Validation

If a validate block is present, an exchange result is validated before storing the result and resuming the execution.
An exchange fails with an error if the result validation fails, see fallback.

exchange('status-api') {
  http {
    url config.api.url
  }
  validate {
    status
  }
  namespace api
}

Exchange Fallback

A fallback defines a workflow, function or expression that is executed when an exchange fails with an error.
This may happen due to a connector error response, timeout or a failed result validation.

exchange('status-api') {
  http {
    url config.api.url
  }
  fallback {
    route 'Error'
  }
}

Result handlers

An exchange command can specify one on more result handlers

  • onError() executed when the result is error
  • onSuccess() executed when the result is success

In functions, each result handler can define an inline function that is executed when the handler condition is match.
In workflows, each result handler can define an inline workflow.

E.g. handling an exchange error in a workflow

exchange('idv') {
  connector 'idv'
}.onError {
    error ->
      route('error') {
        export error
      }
}

E.g. handling an exchange error in a function

exchange('idv') {
  connector 'idv'
}.onError {
  error ->
    function('store-error') {
      input error
      async()
    } 
    error(it.message)
}

The built-in http connector provides additional handlers

  • onStatus(status) executed when response status code matches the given status, the response body is passed as input
  • onResponse() executed for any response, the response entity (headers, status, payload) is passed as input

E.g. handling an http responses

http {
  url "${url}/create"
  method 'POST'
  jsonBody request
}.onStatus(400) {
    match(it.header.code_error in [1108, 2001, 3001, 3020]) {
        decision('INCORECT_CPF', it)
    }
    error(it)
}.onResponse {
  match(it.status == 201) {
      success(it.headers.Location)
  }
  error(it.payload)
}
 

Exchange Timeout

It is possible to set an exchange timeout in seconds. The default value is 30 seconds.

An exchange fails with an error if the underlying connector doesn't respond within the specified timeout

exchange('status-api') {
  connector('custom')
  timeout 10
}

Exchange Retry strategies

An exchange uses a retry strategy to retry when a connector request fails.
The default strategy uses fixed delays between retry attempts.

The following retry strategies are available:

Fixed backoff

Uses fixed delays between retry attempts, given a number of retry attempts and the backoff delay in seconds.

  • retry a number of retry attempts, the default is 5
  • backoff a number of seconds between retries, the default is 5
exchange('fixed-default') {
    http {
      url config.api.url
    }
    fixedBackoffRetry()
}

exchange('fixed-custom') {
    http {
      url config.api.url
    }
    fixedBackoffRetry {
     retry 10
     backoff 2
    } 
}
Exponential backoff

Uses a randomized exponential backoff strategy, given a number of retry attempts and minimum and maximux backoff delays in seconds.

  • retry a number of retry attempts, the default is 5
  • backoff a minimum delay between retry attempts, the default is 5
  • maxBackoff a maximum delay between retry attempts, the default is 50
exchange('exp-default') {
  http {
    url config.api.url
  }
  exponentialBackoffRetry()
}

exchange('exp-custom') {
    http {
      url config.api.url
    }
    exponentialBackoffRetry {
     retry 3
     backoff 5
     maxBackoff 10
    } 
}
No retry

Does not retry when a connector request fails.

exchange('name') {
    http {
      url config.api.url
    }
    noRetry()
}

function

  • like workflow but without user interactions (route, workflow)
  • can be executed asynchronously
  • separate execution with different UUID, data passed using context and input

A function makes it possible to query dynamic data, perform complex calculations or make external calls using exhange().
Functions can be executed from a workflow or from another function.

  • name a function name
  • input a function input
  • context execution context for function execution
  • namespace a namespace to store function result
  • async() the function will be executed asynchronously
function('mobile.lookup') {
 input mobile: '325-135856984'
 context retry: 3 
 namespace lookup
 async()
}

workflow

Executes a sub-workflow synchronously as a separate workflow execution with different UUID.
Data is passed using context and input.
Execution is terminated if sub-workflow terminates with terminal route.

  • name a workflow name
  • input a workflow input
  • context execution context for workflow execution
  • namespace a namespace to store workflow result
workflow('otp') {
 input mobile: '325-135856984'
 context retry: 3 
 namespace otp
}

mapper

An attribute mapper transforms an input into an attribute output using mapper expression, see Mapper. The output gets stored in a namespace if specified.
Can be used for data transformations, calculations and providing default values etc.

mapper('name') {
  input input
  namespace namespace
}

path

Executes a path (workflow snippet) specified by a name. A path is executed as part of the current execution and shares the same execution context.
It is only possible to reference a path defined within the same component as a workflow being executed.

path 'name'

Execution result

There are several ways of terminating an execution and specifying an execution result

  • success() for a success result with payload
  • error() for an error result with payload
  • decision() for a success result with a decision and payload
  • response for exposed function response
  • the result of the last command or expression

Terminates an execution successfully with a payload

success(application)
success firstName: checkIdp.firstName, lastName: checkIdp.lastName

or with an empty payload

success()

Terminates an execution with an error using the specified payload

error "Boom"
error otp

or with an empty error payload

error()

If more granularity in needed, it is possible to terminate an execution successfully using decision().

Terminates an execution successfully with a decision and payload

decision('ACCEPTED', idp)

or with an empty error payload

decision('DENIED')

Exposed function response

Exposed functions can specify an HTTP response details, including body, headers and status code.

response {
    status HttpStatus.ACCEPTED
    header HttpHeaders.LOCATION, 'http://localhost'
    body payload
}
response {
    status 201
    header 'location', "http://localhost"
    header 'x-correlation-id', "345lkigzdf90234as"
}

Execution result handlers

Each function and workflow command can specify one on more result handlers

  • onError() executed when the result is error
  • onSuccess() executed when the result is success or any decision
  • onDecision() executed when the result is a given decision

Only the first matching handler is executed. When a handler is executed, the result payload is passed as an input.

E.g. handling a workflow error

workflow('idv')
    .onError {
      error ->
        route('error') {
          export error
        }
    }

E.g. handling a workflow decision result

workfow('liveness')
    .onDecision('LIVE') {
      payload ->
        workflow('payment') {
          input payload
        }
    }
    .onSuccess {
        workflow('try-again')
    }
    .onError {
      error ->
        route('error') {
          export error
        }
    }

sharable

Generates a sharable token or a link with the token.

A sharable token can be used for starting a new workflow or function execution.
It is possible to specifies a sharable token manually. Otherwise, it is generated as 6 random alpha numerical characters.

A token expires when a corresponding execution is terminated, unless the token is marked as reusable().

  • url if provided generates a link using the sharable token
  • payload an attribute/payload to be shared
  • function a name of function to execute, optionally execution input and context can be provided
  • workflow a name of workflow to execute, optionally execution input and context can be provided
  • latest() a workflow or function reference revision is resolved when sharable token is used (rather than fixed)
  • expired() expires specified token
  • reusable() the token does not expire after corresponding execution terminates
  • namespace asynchronously stores the function or workflow execution result
  • expireIn token will expire after specified duration (use ISO-8601 duration format, default is 24 hours)

Examples of usage are following:

token << sharable { function 'function-name' }

sharable {
   reusable()
   function 'function-name'
   namespace result
}

Generates a sharable token to execute a function named function-name. The token gets stored in the token namespace.

link << sharable {
   url "http://localhost:1234/sharable/$token"
   workflow('workflow-name') {
      context url: 'http://localhost'
      input userId: 'dummy123'
   }
   expireIn 'PT30M'
}

Generates a sharable link to execute a workflow named workflow-name with input and context.
The link gets stored in the link namespace. The link will expire in 30 minutes.

sharable(input.token) {
   payload application
}

Generates a sharable payload that stores application attribute and can be accessed using input.token token.

sharable('vJRRTX') {
   expired()
}

Expires a specific sharable token.

sharable(execution.sharable) {
   expired()
}

Expires a sharable token that was used to execute the current execution.

Exporting namespaces

A context attribute namespace can be exported and queried using Execution API

export config

Logging

It is possible to generate application logs from the DSL using the log command

log "message"

The log message is logged using com.zenoo.hub.dsl.executor.DSLLogger class with the following format

{uuid}: DSL logger {executable}: {message}

Example:

a log message using context attributes

log "request.id=${request.request_id}"

Flow control

await

Blocks the current execution and waits until an attribute is set, e.g. by an asynchronous function, exchange or sharable token.

await attribute

match

Executes a DSL script definition when an expression evaluates as true. The expression can contain context attributes.

match (expression) {
    definition
}

exist

Executes a DSL script definition when an attribute is set.

exist (attribute) {
    definition
}

switch / case

The switch statement matches expression with cases and executes the matching case. It's a fallthrough switch-case. You can share the same code for multiple matches or
use the break command. It uses different kinds of matching like, collection case, regular expression case, closure case and equals case.

switch (expression) {
    case "bar":
        route "Bar"
        break
    
    case ~/fo*/:
        route "Foo"
        break

    case [4, 5, 6, 'inList']:
        route "Matched"
        break
        
    default:
        route "Default"
}

loop-until

Executes a DSL script definition until an expression evaluates as true.

loop {
    definition
} until { expression }

The maximum number of attempts can be specified.

loop(3) {
  workflow
} until { expression }

In addition, the attempt counter can be accessed as follows:

loop(3) {
  attempt ->
    route('test') {
      export attempt
    }
} until { expression }