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
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
{{outer{{inner}}}}
Example:
Variables: {
"inner": "name",
"john": "John Doe"
}
{{{{inner}}}} -> {{name}} -> John Doe
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
-
Iterative Processing
- The engine processes the template in multiple passes
- Each pass identifies all
{{...}}
patterns
- Processing continues until no more replacements are needed
-
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"
-
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
-
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
-
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
-
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}}
-
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
Examples
-
Dynamic Operation Selection
{{plugin:text:{{operation}}:hello}}
With variables:
{
"operation": "upper"
}
Result: HELLO
-
Dynamic Environment Variable Lookup
{{plugin:sys:env:{{env_var}}}}
With variables:
{
"env_var": "HOME"
}
Result: /home/user
-
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
- Add your plugin struct to the template package
- 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
-
Error Handling
- Return clear error messages
- Validate all inputs
- Handle edge cases gracefully
-
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
}
-
Security Considerations
- Validate and sanitize inputs
- Avoid shell execution
- Be careful with file operations
- Limit resource usage
-
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
-
Namespace Selection
- Choose clear, descriptive names
- Avoid conflicts with existing plugins
- Group related operations together
-
Operation Names
- Use lowercase names
- Keep names concise but clear
- Be consistent with similar operations
-
Value Format
- Document expected formats
- Use common separators consistently
- Provide examples in comments
-
Error Messages
- Be specific about what went wrong
- Include valid operation examples
- Help users fix the problem
Common Issues and Solutions
-
Missing Variables
Error: missing required variables: [name]
Solution: Provide all required variables using -v=name:value
-
Invalid Plugin Operations
Error: unknown operation 'invalid' for plugin 'text'
Solution: Check plugin documentation for supported operations
-
Plugin Value Format
Error: invalid format for datetime:rel, expected -1d, -2w, etc.
Solution: Follow the required format for plugin values
Contributing
- Fork the repository
- Create your plugin branch
- Implement your plugin following the guidelines
- Add comprehensive tests
- Submit a pull request
Support
For issues and questions:
- Check the debugging output (enable with Debug=true)
- Review the plugin documentation
- Open an issue with:
- Template content
- Variables used
- Expected vs actual output
- Debug logs