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
- Multiple Fields: Transform multiple fields in one operation
- Custom Regex: Add regex-based transformations
- JSON Path: Support nested field paths like
user.profile.name - Chained Operations: Apply multiple transformations in sequence