Skip to content

Simple Transformer Component

A complete example of a data transformation component.

Overview

This component takes input data and transforms it according to configurable rules. It demonstrates:

  • Basic component structure
  • Settings configuration
  • Input/output ports
  • Data transformation

Complete Implementation

go
package transformer

import (
    "context"
    "fmt"
    "strings"

    "github.com/tiny-systems/module/api/v1alpha1"
    "github.com/tiny-systems/module/pkg/module"
)

const ComponentName = "transformer"

// Component struct
type Component struct {
    settings Settings
}

// Settings for the transformer
type Settings struct {
    Operation   string `json:"operation" title:"Operation"
        enum:"uppercase,lowercase,trim,prefix,suffix" default:"uppercase"
        description:"Transformation operation to apply"`
    Prefix      string `json:"prefix,omitempty" title:"Prefix"
        description:"Prefix to add (when operation is 'prefix')"`
    Suffix      string `json:"suffix,omitempty" title:"Suffix"
        description:"Suffix to add (when operation is 'suffix')"`
    FieldPath   string `json:"fieldPath" title:"Field Path" default:"text"
        description:"JSON path to the field to transform"`
}

// Input message
type Input struct {
    Data map[string]any `json:"data" title:"Data" configurable:"true"
        description:"Input data containing the field to transform"`
}

// Output message
type Output struct {
    OriginalData    map[string]any `json:"originalData" title:"Original Data"`
    TransformedData map[string]any `json:"transformedData" title:"Transformed Data"`
    Operation       string         `json:"operation" title:"Operation Applied"`
}

// ErrorOutput for failures
type ErrorOutput struct {
    Error string         `json:"error" title:"Error Message"`
    Data  map[string]any `json:"data" title:"Original Data"`
}

// Ensure Component implements module.Component
var _ module.Component = (*Component)(nil)

// GetInfo returns component metadata
func (c *Component) GetInfo() module.ComponentInfo {
    return module.ComponentInfo{
        Name:        ComponentName,
        Title:       "Transformer",
        Description: "Transforms text data using configurable operations",
        Category:    "Transform",
        Tags:        []string{"transform", "text", "string"},
    }
}

// Ports returns the port definitions
func (c *Component) Ports() []module.Port {
    return []module.Port{
        {
            Name:          v1alpha1.SettingsPort,
            Label:         "Settings",
            Position:      module.PositionTop,
            Source:        true,
            Configuration: Settings{},
        },
        {
            Name:          "input",
            Label:         "Input",
            Position:      module.PositionLeft,
            Source:        true,
            Configuration: Input{},
        },
        {
            Name:          "output",
            Label:         "Output",
            Position:      module.PositionRight,
            Source:        false,
            Configuration: Output{},
        },
        {
            Name:          "error",
            Label:         "Error",
            Position:      module.PositionBottom,
            Source:        false,
            Configuration: ErrorOutput{},
        },
    }
}

// Handle processes incoming messages
func (c *Component) Handle(
    ctx context.Context,
    output module.Handler,
    port string,
    msg any,
) error {
    switch port {
    case v1alpha1.SettingsPort:
        c.settings = msg.(Settings)
        return nil

    case "input":
        return c.handleInput(ctx, output, msg.(Input))

    default:
        return fmt.Errorf("unknown port: %s", port)
    }
}

// handleInput processes the input data
func (c *Component) handleInput(
    ctx context.Context,
    output module.Handler,
    input Input,
) error {
    // Clone the data
    transformedData := make(map[string]any)
    for k, v := range input.Data {
        transformedData[k] = v
    }

    // Get the field to transform
    fieldValue, ok := transformedData[c.settings.FieldPath]
    if !ok {
        return output(ctx, "error", ErrorOutput{
            Error: fmt.Sprintf("field '%s' not found in data", c.settings.FieldPath),
            Data:  input.Data,
        })
    }

    // Convert to string
    text, ok := fieldValue.(string)
    if !ok {
        return output(ctx, "error", ErrorOutput{
            Error: fmt.Sprintf("field '%s' is not a string", c.settings.FieldPath),
            Data:  input.Data,
        })
    }

    // Apply transformation
    var transformed string
    switch c.settings.Operation {
    case "uppercase":
        transformed = strings.ToUpper(text)
    case "lowercase":
        transformed = strings.ToLower(text)
    case "trim":
        transformed = strings.TrimSpace(text)
    case "prefix":
        transformed = c.settings.Prefix + text
    case "suffix":
        transformed = text + c.settings.Suffix
    default:
        transformed = text
    }

    // Update the transformed data
    transformedData[c.settings.FieldPath] = transformed

    // Send output
    return output(ctx, "output", Output{
        OriginalData:    input.Data,
        TransformedData: transformedData,
        Operation:       c.settings.Operation,
    })
}

// Instance creates a new component instance
func (c *Component) Instance() module.Component {
    return &Component{}
}

Usage Example

Flow Configuration

yaml
# In a TinyNode edge configuration
edges:
  - port: _settings
    data:
      operation: "uppercase"
      fieldPath: "message"

  - port: input
    data:
      data:
        message: "{{$.request.body.text}}"
        timestamp: "{{now()}}"

Input Data

json
{
  "data": {
    "message": "hello world",
    "timestamp": 1705312200000000000
  }
}

Output Data

json
{
  "originalData": {
    "message": "hello world",
    "timestamp": 1705312200000000000
  },
  "transformedData": {
    "message": "HELLO WORLD",
    "timestamp": 1705312200000000000
  },
  "operation": "uppercase"
}

Key Patterns Demonstrated

1. Enum Settings

go
Operation string `json:"operation" enum:"uppercase,lowercase,trim,prefix,suffix"`

The enum tag creates a dropdown in the UI.

2. Conditional Settings

The Prefix and Suffix fields are only relevant for certain operations. You could enhance this with conditional visibility.

3. Error Output Port

A separate error port allows downstream handling of failures:

go
{
    Name:     "error",
    Position: module.PositionBottom,
    Source:   false,
}

4. Data Cloning

Always clone input data before modification:

go
transformedData := make(map[string]any)
for k, v := range input.Data {
    transformedData[k] = v
}

Testing

go
func TestTransformer(t *testing.T) {
    comp := &Component{}

    // Initialize settings
    comp.Handle(context.Background(), nil, v1alpha1.SettingsPort, Settings{
        Operation: "uppercase",
        FieldPath: "text",
    })

    // Test transformation
    var result Output
    output := func(ctx context.Context, port string, msg any) error {
        if port == "output" {
            result = msg.(Output)
        }
        return nil
    }

    err := comp.Handle(context.Background(), output, "input", Input{
        Data: map[string]any{"text": "hello"},
    })

    assert.NoError(t, err)
    assert.Equal(t, "HELLO", result.TransformedData["text"])
}

Extension Ideas

  1. Multiple Fields: Transform multiple fields in one operation
  2. Custom Regex: Add regex-based transformations
  3. JSON Path: Support nested field paths like user.profile.name
  4. Chained Operations: Apply multiple transformations in sequence

Build flow-based applications on Kubernetes