Creating a new Transformer
note
All transformers source code exists inside the packages repository.
The ITransformer
Interface
This page elaborates on how to create a new type of transformer. A transformer "must" implement the ITransformer
interface which contains a single method transformer(xmsg: XMessage): Promise<XMessage>
.
Example Implementation
import { XMessage } from "@samagra-x/xmessage";
import { ITransformer } from "../common/transformer.interface";
export class MyOwnTransformer implements ITransformer {
constructor(readonly config: Record<string, any>) { }
async transform(xmsg: XMessage): Promise<XMessage> {
if (!xmsg.transformer) {
xmsg.transformer = {
metaData: {}
};
}
// Perform any transform logic
const calculation = 2 + 2;
// Add the result to xmsg
xmsg.transformer.metaData!.calculationResult = calculation;
// Change the output state
xmsg.transformer.metaData!.state = 'outputState';
// Return the output xmessage
return xmsg;
}
}
Implementation
Implementing a transformer is a 4 step process:
- Implementing a Transformer class.
- Creating a
config.json
file for the Transformer. - Generating
registry.json
file. - Adding the Transformer class to the TransformerFactory.
Step 1: Implementing a Transformer Class
Transformers me belong to one of the 5 classes namely:
- GenericTransformer
- IfElseTransformer
- SwitchCaseTransformer
- StateRestoreTransformer
- RetryTransformer
For creating a transformer create a new folder with the name of the transformer inside the relevant transformer class. Inside the folder create a new transformer file, for example, my_transformer.transformer.ts
.
Creating a GenericTransformer class Transformer
A GenericTransformer can perform any generic function on an XMessage. A GenericTransformer works on 2 states, that is, the onSuccess
and onError
states. A transformer transform
function must resolve successfully with a response XMessage or else reject the promise. If the transform function resolves, onSuccees
target is called, else onError
target is called.
Example Implementation
import { XMessage } from "@samagra-x/xmessage";
import { ITransformer } from "../common/transformer.interface";
export class MyOwnGenericTransformer implements ITransformer {
constructor(readonly config: Record<string, any>) { }
async transform(xmsg: XMessage): Promise<XMessage> {
if (!xmsg.transformer) {
xmsg.transformer = {
metaData: {}
};
}
// Perform any transform logic
const calculation = 2 + 2;
// Add the result to xmsg
xmsg.transformer.metaData!.calculationResult = calculation;
// Return the output xmessage
return xmsg;
}
}
Creating an IfElseTransformer class Transformer
A IfElseTransformer outputs a binary if
or else
state.
Example Implementation
import { XMessage } from "@samagra-x/xmessage";
import { ITransformer } from "../common";
export class MyOwnIfElseTransformer implements ITransformer {
constructor(readonly config: Record<string, any>) { }
async transform(xmsg: XMessage): Promise<XMessage> {
if (!xmsg.transformer) {
xmsg.transformer = {
metaData: {}
}
}
// Update the state in metaData
xmsg.transformer.metaData!.state = Math.random() > 0.5 ? 'if' : 'else';
return xmsg;
}
}
Creating a RetryTransformer class Transformer
A RetryTransformer outputs a retry
or error
state. It "must" update the metaData.retryCount
property after retrying a transformer node.
Example Implementation
import { XMessage } from "@samagra-x/xmessage";
import { ITransformer } from "../common";
export class MyOwnRetryTransformer implements ITransformer {
constructor(readonly config: Record<string, any>) { }
async transform(xmsg: XMessage): Promise<XMessage> {
if (!xmsg.transformer) {
xmsg.transformer = {
metaData: {}
}
}
// Check value of `retryCount`.
if (!xmsg.transformer.metaData!.retryCount || xmsg.transformer.metaData!.retryCount < (this.config.retries ? 0)) {
// Modify value of `retryCount`.
xmsg.transformer.metaData!.retryCount = (xmsg.transformer.metaData!.retryCount || 0) + 1;
// Return `retry` as the result state.
xmsg.transformer.metaData!.state = 'retry';
}
else {
// Delete `retryCount` and return `error` as result state.
delete xmsg.transformer.metaData!.retryCount;
xmsg.transformer.metaData!.state = 'error';
}
console.log(`SIMPLE_RETRY count: ${xmsg.transformer.metaData!.retryCount}`);
return xmsg;
}
}
Creating a SwitchCaseTransformer class Transformer
A SwitchCaseTransformer outputs an arbitary number of states.
Example Implementation
import { XMessage } from "@samagra-x/xmessage";
import { ITransformer } from "../common";
export class MyOwnSwitchCaseTransformer implements ITransformer {
constructor(readonly config: Record<string, any>) { }
async transform(xmsg: XMessage): Promise<XMessage> {
if (!xmsg.transformer) {
xmsg.transformer = {
metaData: {}
}
}
const myStates = ['these', 'are', 'my', 'states'];
// Update the state in metaData
xmsg.transformer.metaData!.state = myStates[Math.floor(Math.random() * 4)];
return xmsg;
}
}
Step 2: Creating a config file
A config file is a file that contains the meta data about a transformer. The data inside a config file is added to the global registry.json
file. In the same folder parallel to the my_transformer.transformer.ts
file create a config.json
file. The spec for creating a config.json
file is validated using zod and is as follows.
const transformerClassEnum = z.nativeEnum(TransformerClass);
const transformerTypeEnum = z.nativeEnum(TransformerType);
const transformerSpec = z.object({
name: z.string().min(1),
class: transformerClassEnum,
type: transformerTypeEnum,
description: z.string().min(1),
config: z.object({
required: z.record(z.string().min(1), z.string().min(1)),
optional: z.record(z.string().min(1), z.string().min(1)),
conditional: z.record(
z.string().min(1),
z.object({
type: z.string().min(1),
ifAbsent: z.string().min(1).array().optional(),
ifPresent: z.string().min(1).array().optional(),
}),
),
}),
version: z.string(),
});
Here is an example of CodeRunner Transformer config file.
{
"name": "Code Runner Transformer",
"class": "GenericTransformer",
"type": "CODE_RUNNER",
"description": "A code runner capable of running custom JS code.",
"config": {
"required": {
"code": "string"
},
"optional": { },
"conditional": { }
},
"version": "0.0.1"
}
Step 3: Running the registry generator
Inside the root folder of the transformers folder, there exists a generator script called registry_generator.ts
which is responsible for extracting all config files and adding it to the registry.json
file. The registry.json
is an auto-generated file and must not be modified manually. You need to run this script to validate that your config file is correct and add it to the registry automatically.
To run the script simply use this command:
npx tsx registry_generator.ts
If the script executes without any error, then congratulations, your config file is correct.
note
Do not forget to add registry.json
file to your git commit or the CI will fail as it compares the latest registry by running the generator script.
Step 4: Adding your transformer to the Factory
Inside the common/transformer.types.ts
file, you need to declare your transformer as one of the transformer types. For this, add a new entry in TransformerType
enum as well as to TransformerMapping
object which maps your transformer type to a class.
Once this is done you need to open XStateFactory in Orchestrator and look for getTransformerObject()
function. This function is responsible for creating an instance of your Transformer class. Go ahead and add an entry for your new TransformerType.
Once this is all wired up, you should be ready to use your own Transformer ๐.