Declaring Transforms
Transforms
A transform is a pure function that transforms one message to another message. It allows you to integrate software components that communicate with different message standards.
Like a function, a transform accepts input and returns output, executing the expressions or statements within it. The input parameter must be a message, and the return type must be a message. See Message Types. Transforms can also accept contextual parameters — a way of defining additional, context specific information that a transform needs. More on that below.
A transform may be declared using the transform
keyword.
Syntax
flex
transform NAME( input_name : INPUT_TYPE ) -> RETURN_TYPE {BODY;}
The last expression of a transform is implicitly returned, so the body should end with a RETURN_TYPE object.
Example
Imagine you have a software component that provides a temperature value in Celsius, but another software component in your system requires that the temperature value be in Fahrenheit. Let's create a transform so these software components can be connected together.
Declare a transform
named Celsius2Fahrenheit
that takes a Celsius
message struct and returns a Fahrenheit
message struct:
flex
message struct Celsius {temp: float64;}message struct Fahrenheit {temp: float64;}transform Celsius2Fahrenheit(c : Celsius) -> Fahrenheit {Fahrenheit {temp = ((c.temp * 9.0) / 5.0) + 32.0;};}
Variables
Variables may be declared within the scope of a transform using the let
keyword. See Function Variables.
flex
transform Celsius2Fahrenheit(c : Celsius) -> Fahrenheit {let f = ((c.temp * 9.0) / 5.0) + 32.0;Fahrenheit {temp = f;};}
Conditional Statements
Conditional statements may be included using the if
, else if
, and else
keywords. See Function Conditional Statements.
Contextual Parameters
A contextual parameter is a way of defining additional, context specific information that a transform needs. In the signature of a transform you can define this extra context, and then use those parameters in the implementation of your transform. This information is system (or integration) specific, so the generated C++ code has some holes in it for the system integrator to fill in.
Contextual Parameters are defined in a transform using the given ( ... )
keyword.
Syntax
flex
transform NAME(input_name : INPUT_TYPE ) -> RETURN_TYPE given (?contextual_name : CONTEXTUAL_TYPE) {BODY;}
You can pass in multiple contextual parameters, separated by commas:
flex
transform NAME(input_name : INPUT_TYPE ) -> RETURN_TYPE given (?contextual_name1 : CONTEXTUAL_TYPE1, ?contextual_name2 : CONTEXTUAL_TYPE2) {BODY;}
Example
Let's transform Celsius
to Fahrenheit
like in the previous example. However, now Fahrenheit
is different. It requires a current timestamp value, but Celsius
does not provide it.
Here are the two message structs:
flex
message struct Celsius {temp: float64;}message struct Fahrenheit {temp: float64;timestamp: uint64;}
First, we'll add some definitions to capture the kind of contextual information we need for our transform. Let's create a struct
named CurrentTime
that will contain the timestamp value. Then let's wrap CurrentTime
in a newtype
named ContextualTime
. A contextual parameter must be provided to a transform as a newtype
. See New Types.
flex
struct CurrentTime {time : uint64;}newtype ContextualTime {value : CurrentTime;}
Now we can use ContextualTime
as a contextual parameter in the transform:
flex
transform Celsius2Fahrenheit(c : Celsius) -> Fahrenheit given (?ct: ContextualTime) {Fahrenheit {temp = ((c.temp * 9.0) / 5.0) + 32.0;timestamp = ?ct.value.time;};}
Using a contextual parameter, you can write a transform that is a pure function, and allow any side effects to happen outside of the world of Flex.
Provider Files
When you run a workflow and generate C++ code for your transform, a Provider
file is generated for each contextual parameter. To complete the system integration, you'll need to open the Provider
file and write C++ code to define where the value for the contextual parameter comes from.
To locate the Provider
file, open the generated artifact and open the transform
folder. In our example, the file is named Provider_CelsiusToFahrenheit_CurrentTime.hpp
because the parameter was named CurrentTime
and the Flex module was named CelsiusToFahrenheit
. Open and edit it.
The generated Provider
file contains a TODO section where you'll need to remove the fprintf
and abort
statements, and define a value for param
.
In our example, we need to set the parameter to an integer time value so we'll include a C++ function to get the current unix time.
Replace the // TODO
section with this:
cpp
auto t1 = std::chrono::system_clock::now();param._time = std::chrono::duration_cast<std::chrono::seconds>(t1.time_since_epoch()).count();
The first line creates a variable t1
and sets it to the current system time.
The second line sets param._time
to t1
cast in seconds. This will set a value like 1657722245
to param._time
.
param
is an alias for CelsiusToFahrenheit::CurrentTime
, so what is being set is CelsiusToFahrenheit::CurrentTime._time
.
In our Flex code above, we defined the contextual parameter as a struct named CurrentTime
with a field named time
. In the generated C++ code that field is named _time
.
Flex:
flex
struct CurrentTime {time : uint64;}
To see the C++ struct open the file transform/include/CelsiusToFahrenheit.hpp
cpp
namespace CelsiusToFahrenheit{struct CurrentTime{CurrentTime(){_time = 0;}CelsiusToFahrenheit::CurrentTime& operator=(const CelsiusToFahrenheit::CurrentTime& that){this->copy(that);return *this;}CurrentTime(const CelsiusToFahrenheit::CurrentTime& that) : CurrentTime(){if(this == &that){return ;}this->copy(that);}virtual void copy(const CelsiusToFahrenheit::CurrentTime& from){this->_time = from._time;}~CurrentTime(){}uint64_t _time;};}