Fabric Template System

Quick Start

echo "Hello {{name}}!" | fabric -v=name:World

Overview

The Fabric Template System provides a powerful and extensible way to handle variable substitution and dynamic content generation through a plugin architecture. It uses a double-brace syntax ({{}}) for variables and plugin operations, making it both readable and flexible.

Basic Usage

Variable Substitution

The template system supports basic variable substitution using double braces:

Hello {{name}}!
Current role: {{role}}

Variables can be provided via:

  • Command line arguments: -v=name:John -v=role:admin
  • YAML front matter in input files
  • Environment variables (when configured)

Special Variables

  • {{input}}: Represents the main input content
    Here is the analysis:
    {{input}}
    End of analysis.
    

Nested Tokens and Resolution

Basic Nesting

The template system supports nested tokens, where inner tokens are resolved before outer ones. This enables complex, dynamic template generation.

Simple Variable Nesting

{{user_{{inner}}}}

Example:
Variables: {
  "inner": "name",
  "user_name": "John Doe"
}
{{user_{{inner}}}} -> {{user_name}} -> John Doe

The outer token must have template text in addition to the inner token. In {{{{inner}}}}, the body is only the value of inner, so the result is the literal text {{name}}.

Nested Plugin Calls

{{plugin:text:upper:{{plugin:sys:env:USER}}}}
First resolves: {{plugin:sys:env:USER}} -> "john"
Then resolves: {{plugin:text:upper:john}} -> "JOHN"

How Nested Resolution Works

  1. Single Pass

    • The engine reads the template one time, from left to right
    • Each {{...}} token in the template resolves one time
    • Inner tokens resolve before the token that contains them
    • The engine does not scan text from a variable, a plugin or an extension, so a {{...}} in that text stays as literal text
    • In {{{name}}} (three braces), the outer braces are literal text: {John Doe}
    • A token with a single brace, for example {{a}{b}}, stays as literal text
  2. Resolution Order

    Original: {{plugin:text:upper:{{user}}}}
    Step 1: Found {{user}} -> "john"
    Step 2: Now have {{plugin:text:upper:john}}
    Step 3: Final result -> "JOHN"
    
  3. Complex Nesting Example

    {{plugin:text:{{case}}:{{plugin:sys:env:{{varname}}}}}}
    
    With variables:
    {
      "case": "upper",
      "varname": "USER"
    }
    
    Resolution steps:
    1. {{varname}} -> "USER"
    2. {{plugin:sys:env:USER}} -> "john"
    3. {{case}} -> "upper"
    4. {{plugin:text:upper:john}} -> "JOHN"
    

Important Considerations

  1. Depth Limitations

    • While nesting is supported, avoid excessive nesting for clarity
    • Complex nested structures can be hard to debug
    • Consider breaking very complex templates into smaller parts
  2. Variable Resolution

    • Inner variables must resolve to valid values for outer operations
    • Error messages will point to the innermost failed resolution
    • Debug logs show the step-by-step resolution process
  3. Plugin Nesting

    # Valid:
    {{plugin:text:upper:{{plugin:sys:env:USER}}}}
    
    # Also Valid:
    {{plugin:text:{{operation}}:{{value}}}}
    
    # Invalid (plugin namespace cannot be dynamic):
    {{plugin:{{namespace}}:operation:value}}
    
  4. Debugging Nested Templates

    Debug = true  // Enable debug logging
    
    Template: {{plugin:text:upper:{{user}}}}
    Debug output:
    > Processing variable: user
    > Replacing {{user}} with john
    > Plugin call:
    >   Namespace: text
    >   Operation: upper
    >   Value: john
    > Plugin result: JOHN
    
  5. Variables in Plugin Tokens

    • A token in the template can use a variable value as a plugin operation or value. For example, {{plugin:sys:env:{{env_var}}}} reads the environment variable that env_var names
    • A REST client can set the variables of a request. Thus a pattern for REST use must not take a plugin namespace, operation or value from a variable
  6. Variables in User Input

    • With --input-has-vars, Fabric replaces the variables in the user input
    • A plugin or extension token in the user input stays as literal text, and the plugin or extension does not run. The input can contain text from other sources, for example a web page or a transcript

Examples

  1. Dynamic Operation Selection

    {{plugin:text:{{operation}}:hello}}
    
    With variables:
    {
      "operation": "upper"
    }
    
    Result: HELLO
    
  2. Dynamic Environment Variable Lookup

    {{plugin:sys:env:{{env_var}}}}
    
    With variables:
    {
      "env_var": "HOME"
    }
    
    Result: /home/user
    
  3. Nested Date Formatting

    {{plugin:datetime:{{format}}:{{plugin:datetime:now}}}}
    
    With variables:
    {
      "format": "full"
    }
    
    Result: Wednesday, November 20, 2024
    

Plugin System

Plugin Syntax

Plugins use the following syntax:

{{plugin:namespace:operation:value}}
  • namespace: The plugin category (e.g., text, datetime, sys)
  • operation: The specific operation to perform
  • value: Optional value for the operation

Built-in Plugins

Text Plugin

Text manipulation operations:

{{plugin:text:upper:hello}}  -> HELLO
{{plugin:text:lower:HELLO}}  -> hello
{{plugin:text:title:hello world}} -> Hello World

DateTime Plugin

Time and date operations:

{{plugin:datetime:now}}       -> 2024-11-20T15:04:05Z
{{plugin:datetime:today}}     -> 2024-11-20
{{plugin:datetime:rel:-1d}}   -> 2024-11-19
{{plugin:datetime:month}}     -> November

System Plugin

System information:

{{plugin:sys:hostname}}   -> server1
{{plugin:sys:user}}       -> currentuser
{{plugin:sys:os}}         -> linux
{{plugin:sys:env:HOME}}   -> /home/user

Developing Plugins

Plugin Interface

To create a new plugin, implement the following interface:

type Plugin interface {
    Apply(operation string, value string) (string, error)
}

Example Plugin Implementation

Here's a simple plugin that performs basic math operations:

package template

type MathPlugin struct{}

func (p *MathPlugin) Apply(operation string, value string) (string, error) {
    switch operation {
    case "add":
        // Parse value as "a,b" and return a+b
        nums := strings.Split(value, ",")
        if len(nums) != 2 {
            return "", fmt.Errorf("add requires two numbers")
        }
        a, err := strconv.Atoi(nums[0])
        if err != nil {
            return "", err
        }
        b, err := strconv.Atoi(nums[1])
        if err != nil {
            return "", err
        }
        return fmt.Sprintf("%d", a+b), nil
    
    default:
        return "", fmt.Errorf("unknown math operation: %s", operation)
    }
}

Registering a New Plugin

  1. Add your plugin struct to the template package
  2. Register it in template.go:
var (
    // Existing plugins
    textPlugin = &TextPlugin{}
    datetimePlugin = &DateTimePlugin{}
    
    // Add your new plugin
    mathPlugin = &MathPlugin{}
)

// Update the plugin handler in ApplyTemplate
switch namespace {
    case "text":
        result, err = textPlugin.Apply(operation, value)
    case "datetime":
        result, err = datetimePlugin.Apply(operation, value)
    // Add your namespace
    case "math":
        result, err = mathPlugin.Apply(operation, value)
    default:
        return "", fmt.Errorf("unknown plugin namespace: %s", namespace)
}

Plugin Development Guidelines

  1. Error Handling

    • Return clear error messages
    • Validate all inputs
    • Handle edge cases gracefully
  2. Debugging

    • Use the debugf function for logging
    • Log entry and exit points
    • Log intermediate calculations
func (p *MyPlugin) Apply(operation string, value string) (string, error) {
    debugf("MyPlugin operation: %s value: %s\n", operation, value)
    // ... plugin logic ...
    debugf("MyPlugin result: %s\n", result)
    return result, nil
}
  1. Security Considerations

    • Validate and sanitize inputs
    • Avoid shell execution
    • Be careful with file operations
    • Limit resource usage
  2. Performance

    • Cache expensive computations
    • Minimize allocations
    • Consider concurrent access

Testing Plugins

Create tests for your plugin in plugin_test.go:

func TestMathPlugin(t *testing.T) {
    plugin := &MathPlugin{}
    
    tests := []struct {
        operation string
        value     string
        expected  string
        wantErr   bool
    }{
        {"add", "5,3", "8", false},
        {"add", "bad,input", "", true},
        {"unknown", "value", "", true},
    }
    
    for _, tt := range tests {
        result, err := plugin.Apply(tt.operation, tt.value)
        if (err != nil) != tt.wantErr {
            t.Errorf("MathPlugin.Apply(%s, %s) error = %v, wantErr %v",
                tt.operation, tt.value, err, tt.wantErr)
            continue
        }
        if result != tt.expected {
            t.Errorf("MathPlugin.Apply(%s, %s) = %v, want %v",
                tt.operation, tt.value, result, tt.expected)
        }
    }
}

Best Practices

  1. Namespace Selection

    • Choose clear, descriptive names
    • Avoid conflicts with existing plugins
    • Group related operations together
  2. Operation Names

    • Use lowercase names
    • Keep names concise but clear
    • Be consistent with similar operations
  3. Value Format

    • Document expected formats
    • Use common separators consistently
    • Provide examples in comments
  4. Error Messages

    • Be specific about what went wrong
    • Include valid operation examples
    • Help users fix the problem

Common Issues and Solutions

  1. Missing Variables

    Error: missing required variables: [name]
    Solution: Provide all required variables using -v=name:value
    
  2. Invalid Plugin Operations

    Error: unknown operation 'invalid' for plugin 'text'
    Solution: Check plugin documentation for supported operations
    
  3. Plugin Value Format

    Error: invalid format for datetime:rel, expected -1d, -2w, etc.
    Solution: Follow the required format for plugin values
    

Contributing

  1. Fork the repository
  2. Create your plugin branch
  3. Implement your plugin following the guidelines
  4. Add comprehensive tests
  5. Submit a pull request

Support

For issues and questions:

  1. Check the debugging output (enable with Debug=true)
  2. Review the plugin documentation
  3. Open an issue with:
    • Template content
    • Variables used
    • Expected vs actual output
    • Debug logs