Post-Processing System
The post-processing system provides a generic framework for transforming generated content after template rendering but before writing to disk.
Overview
Post-processors allow you to:
- Format and organize code (imports, whitespace)
- Add generated file headers
- Apply custom transformations
- Validate generated content
- Perform file-type specific optimizations
Basic Usage
import (
"github.com/cpcf/weft/engine"
"github.com/cpcf/weft/processors"
)
// Create engine and add processors
eng := engine.New()
// Add built-in processors
eng.AddPostProcessor(processors.NewGoImports()) // Fix Go imports
eng.AddPostProcessor(processors.NewTrimWhitespace()) // Clean whitespace
eng.AddPostProcessor(processors.NewAddGeneratedHeader("myapp", ".go", ".js")) // Add headers
// Generate files
err := eng.RenderDir(ctx, "templates", data)
Built-in Processors
Go Imports (processors.NewGoImports())
Fixes import statements and formats Go code using goimports:
processor := processors.NewGoImports()
// Customize options
processor.TabWidth = 4
processor.TabIndent = false
processor.AllErrors = true
Trim Whitespace (processors.NewTrimWhitespace())
Removes trailing whitespace from all lines:
eng.AddPostProcessor(processors.NewTrimWhitespace())
Adds "Code generated" headers to files:
// Add header to specific file types
eng.AddPostProcessor(processors.NewAddGeneratedHeader("myapp", ".go", ".java"))
// Add header to all files
eng.AddPostProcessor(processors.NewAddGeneratedHeader("myapp"))
Regex Replace (processors.NewRegexReplace())
Apply regex transformations:
// Replace TODO comments with DONE
processor, err := processors.NewRegexReplace(`TODO: (.+)`, "DONE: $1")
if err != nil {
log.Fatal(err)
}
eng.AddPostProcessor(processor)
// Limit to specific file types
processor.WithFilePattern(`\.go$`)
Custom Processors
Implement the postprocess.Processor interface:
type CustomProcessor struct{}
func (p *CustomProcessor) ProcessContent(filePath string, content []byte) ([]byte, error) {
// Apply custom transformation
if strings.HasSuffix(filePath, ".go") {
// Add custom Go code transformations
transformed := doCustomTransform(content)
return transformed, nil
}
return content, nil // Leave other files unchanged
}
// Add to engine
eng.AddPostProcessor(&CustomProcessor{})
Function-based Processors
For simple transformations, use function processors:
eng.AddPostProcessorFunc(func(filePath string, content []byte) ([]byte, error) {
// Convert line endings to Unix style
return bytes.ReplaceAll(content, []byte("\r\n"), []byte("\n")), nil
})
Advanced Usage
Chaining Multiple Processors
Processors run in the order they're added:
eng.AddPostProcessor(processors.NewGoImports()) // 1. Fix imports first
eng.AddPostProcessor(processors.NewTrimWhitespace()) // 2. Clean whitespace
eng.AddPostProcessor(processors.NewAddGeneratedHeader("myapp", ".go")) // 3. Add header last
Error Handling
Processors that fail log a warning but don't stop generation:
// This processor might fail but won't break the build
eng.AddPostProcessor(processors.NewGoImports())
File-type Specific Processing
// Only process Go files
eng.AddPostProcessorFunc(func(filePath string, content []byte) ([]byte, error) {
if !strings.HasSuffix(filePath, ".go") {
return content, nil
}
// Go-specific processing here
return processGoFile(content), nil
})
Examples in Other Languages
Java Processor
type JavaFormatter struct{}
func (j *JavaFormatter) ProcessContent(filePath string, content []byte) ([]byte, error) {
if !strings.HasSuffix(filePath, ".java") {
return content, nil
}
// Format Java code using google-java-format or similar
return formatJavaCode(content), nil
}
Python Processor
type PythonFormatter struct{}
func (p *PythonFormatter) ProcessContent(filePath string, content []byte) ([]byte, error) {
if !strings.HasSuffix(filePath, ".py") {
return content, nil
}
// Format Python code using black or autopep8
return formatPythonCode(content), nil
}
Best Practices
- Order Matters: Add processors in logical order (format → clean → annotate)
- File Type Checking: Always check file extensions before processing
- Error Handling: Return original content on errors rather than failing
- Performance: Keep processors lightweight for large codebases
- Idempotency: Ensure processors can run multiple times safely
Integration with CI/CD
Post-processors integrate seamlessly with build pipelines:
// In your generator
func main() {
eng := engine.New()
// Add standard processors
eng.AddPostProcessor(processors.NewGoImports())
eng.AddPostProcessor(processors.NewTrimWhitespace())
eng.AddPostProcessor(processors.NewAddGeneratedHeader(os.Args[0]))
// Add custom validation
eng.AddPostProcessorFunc(validateGeneratedFiles)
if err := eng.RenderDir(ctx, "templates", data); err != nil {
log.Fatal(err)
}
}
This ensures generated code is properly formatted, validated, and ready for version control.