Skip to main content

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.

Provider File

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.

Provider File

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.

note

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;
};
}