Template Library
Introduction
This library provides a framework for building templates. One can create .tengo file,
import this library, and call several functions,
such as
defineOutputs(), body() etc.
Terminology
A template is a resource representing
a compiled text file with tengo code in Platforma Backend.
A template's renderer is a resource that represents a template with defined arguments.
A workflow controller waits for the template's renderer to become ready for execution and then runs the code with the specified arguments.
One can use render library
to create templates' renderers.
There are two types of templates: simple and ephemeral.
A simple template waits until all its inputs are filled and only then is executed. Platforma Backend caches results of its execution, and the next time it encounters a new template's renderer with the same arguments, it retrieves the results from the cache and the renderer is not executed again.
As an example, running arbitrary commands with exec library is implemented with simple templates, and such commands won't be run twice.
Ephemeral template has much more control over when it should be executed.
awaitState can be used to define a state of arguments suitable for running a specific template. For example, one can await a state where a particular argument must have a certain field with a value, while other fields do not need to exist.
For a more practical example, all workflows instantiated via workflow library are implemented as ephemeral templates.
This is necessary because results from previous blocks can be received at different times:
a slow result from a complex analysis pipeline
and a fast result from the client's keyboard.
Sources
https://github.com/milaboratory/platforma/blob/main/sdk/workflow-tengo/src/tpl/index.lib.tengo
API
Import
One usually imports this library under alias self,
since it represents an instance of this template.
self := import("@platforma-sdk/workflow-tengo:tpl")
defineOutputs
Defines current template output(s) names. For pure templates, all outputs must be defined before the template body, and only registered outputs can be used inside. Optional for ephemeral templates.
Arguments:
names:...string- a list of output names.
Example:
self.defineOutputs("qc", "reports", "clones", "log", "clns")
body
Routes a template. One should provide a callback that will be executed when all of the arguments will be in ready state.
For simple templates it is when
all of the arguments are ready,
and for ephemeral templates it is a condition from awaitState().
Arguments:
bodyFn:func(inputs): map[string]outputs- the callback that gets all inputs as arguments and should return a map of all names to its outputs.
Example:
It's an example of defining a template and calling from another code using render library:
// caller template:
...
result := render.create(assets.importTemplate(":calleeTemplate"), {
argument1: 11,
argument2: 31,
nested: [{a: 1, b: 2}],
smartResourceOrField: resource1
})
result.output("result1", 24*60*60*1000) // == 42
...
// callee template:
self := import("@platforma-sdk/workflow-tengo:tpl")
self.defineOutputs("result1", "result2")
self.body(func(inputs) {
return {
// This will be 42
result1: inputs.argument1 + inputs.argument2,
// This will be a value that depends on a content of resource1 smart resource.
result2: inputs.resource1.getValue() + inputs.nested.a
}
})
prepare
Prepare additional resources for the template body. The body function of the workflow will only be executed when the references returned from the prepare method will be resolved into ready resources. The resulting resources will be passed as an input to the body function.
Arguments:
cb:func(inputs): map[string]reference- a callback function returning a map of references to be resolved
Example:
//
// We need to resolve the ref into the actual sequence set
// resource in order to use it in the body.
//
self.prepare(func(args) {
return {
resolvedRef: self.resolve(args.ref, { errIfMissing: true })
}
})
self.body(func(args) {
args.resolvedRef // this will have a resolved reference
})
inputs
Parses template inputs and returns a tengo map of them. The parser:
- converts all json resources (potentially nested) into a tengo objects (maps)
- converts all pl.maps (resource maps) into tengo maps
Return:
inputs:map[string]any- a map of the parsed template inputs
Example:
self.body(func(inputs) {
ll.assert(self.inputs() == inputs, "inputs and self.inputs() are both the same map")
})
rawInputs
It's the same as inputs,
but doesn't parse all inputs and returns them as fields.
It is useful when one need to pass map or value resources as-is and doesn't need parsed maps.
Return:
result:map[string]field: - a map of inputs to the fields of the input map.
hasInput
Shows whether the template's renderer has this input or not.
Return:
hasInput:bool: whether there is input with a given name
validateInputs
Validate inputs schema before passing inputs to body. See validation library for more details.
Arguments:
schema:validationSchema
awaitState
In an ephemeral template, body will only be executed when corresponding input state is reached.
Arguments:
path:...stringrequestedState:string
Allowed requested states:
Exists- field existsSet- field points to existing resourceInputsLocked- field points to resource with locked inputsAllInputsSet- field points to resource with all inputs setResourceReady- field points to ready resource
Example:
self.awaitState("AllInputsSet")- all inputs of the inputs map must be set.self.awaitState("inputName1", "ResourceReady")- resource in the inputinputName1must be ready.self.awaitState("inputName1", "nestedField", "Set")- nested input field in the inputinputName1must exist and point to some resource.self.awaitState("inputName1", { wildcard: "*" }, "InputsLocked")- nested resource by theinputName1field must exist and have locked inputs and all resources referenced by those fields must have all inputs locked.self.awaitState("inputName1", { match: "^some_prefix\\." }, "InputsLocked")- nested resource by theinputName1field must exist and have locked inputs and all resources referenced by inputs with must have all inputs locked.
Example, the following two expressions are equivalent:
self.awaitState("inputName1", { wildcard: "*" }, "Set")self.awaitState("inputName1", { match: "^some_prefix\\." }, "Set")