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
-
Context attributes for storing and working with JSON-like data
-
DSL commands implement the basic building blocks of a digital onboarding
- route represents user interactions
- exchange makes external calls using connectors
- http makes external calls using connectors
- workflow executes child workflows
- function executes child functions
- mapper used for data mappings and transformations
- sharable generates and manages sharable links
- success / error / decision / response terminates the current execution
- logging for DSL logging
- Flow control commands allow for conditional execution
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 erroronSuccess()
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 givenstatus
, the response body is passed as inputonResponse()
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 payloaderror()
for an error result with payloaddecision()
for a success result with a decision and payloadresponse
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 erroronSuccess()
executed when the result is success or any decisiononDecision()
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 }
Updated 4 months ago