Skip to content

Advanced Usage

This guide covers advanced usage patterns and best practices for the Erlang Rebar Config Parser, including performance optimization, error handling strategies, and complex use cases.

Performance Optimization

Memory Management

When working with large configurations or processing many files, consider memory usage:

go
func processLargeConfigs(configPaths []string) error {
    for _, path := range configPaths {
        // Process one config at a time to limit memory usage
        config, err := parser.ParseFile(path)
        if err != nil {
            log.Printf("Failed to parse %s: %v", path, err)
            continue
        }
        
        // Process the configuration
        processConfig(config)
        
        // Allow garbage collection
        config = nil
        runtime.GC()
    }
    
    return nil
}

Concurrent Processing

For processing multiple configurations concurrently:

go
func processConcurrently(configPaths []string, maxWorkers int) {
    jobs := make(chan string, len(configPaths))
    results := make(chan ConfigResult, len(configPaths))
    
    // Start workers
    for w := 0; w < maxWorkers; w++ {
        go configWorker(jobs, results)
    }
    
    // Send jobs
    for _, path := range configPaths {
        jobs <- path
    }
    close(jobs)
    
    // Collect results
    for i := 0; i < len(configPaths); i++ {
        result := <-results
        if result.Error != nil {
            log.Printf("Error processing %s: %v", result.Path, result.Error)
        } else {
            log.Printf("Successfully processed %s", result.Path)
        }
    }
}

type ConfigResult struct {
    Path   string
    Config *parser.RebarConfig
    Error  error
}

func configWorker(jobs <-chan string, results chan<- ConfigResult) {
    for path := range jobs {
        config, err := parser.ParseFile(path)
        results <- ConfigResult{
            Path:   path,
            Config: config,
            Error:  err,
        }
    }
}

Advanced Error Handling

Custom Error Types

Create custom error types for better error handling:

go
type ConfigError struct {
    Type    string
    Path    string
    Line    int
    Column  int
    Message string
    Cause   error
}

func (e *ConfigError) Error() string {
    if e.Path != "" {
        return fmt.Sprintf("%s error in %s: %s", e.Type, e.Path, e.Message)
    }
    return fmt.Sprintf("%s error: %s", e.Type, e.Message)
}

func (e *ConfigError) Unwrap() error {
    return e.Cause
}

func parseWithDetailedErrors(path string) (*parser.RebarConfig, error) {
    config, err := parser.ParseFile(path)
    if err != nil {
        // Enhance error with context
        configErr := &ConfigError{
            Type:    "Parse",
            Path:    path,
            Message: err.Error(),
            Cause:   err,
        }
        
        // Try to extract line/column information if available
        if strings.Contains(err.Error(), "line") {
            // Parse line information from error message
            // This is implementation-specific
        }
        
        return nil, configErr
    }
    
    return config, nil
}

Error Recovery Strategies

Implement strategies to recover from partial parse failures:

go
func parseWithRecovery(content string) (*parser.RebarConfig, []error) {
    var errors []error
    
    // Try to parse the entire content first
    config, err := parser.Parse(content)
    if err == nil {
        return config, nil
    }
    
    errors = append(errors, err)
    
    // If parsing fails, try to parse individual terms
    lines := strings.Split(content, "\n")
    var validTerms []parser.Term
    
    for i, line := range lines {
        line = strings.TrimSpace(line)
        if line == "" || strings.HasPrefix(line, "%") {
            continue // Skip empty lines and comments
        }
        
        // Try to parse individual line as a term
        if strings.HasSuffix(line, ".") {
            termConfig, err := parser.Parse(line)
            if err != nil {
                errors = append(errors, fmt.Errorf("line %d: %w", i+1, err))
            } else if len(termConfig.Terms) > 0 {
                validTerms = append(validTerms, termConfig.Terms...)
            }
        }
    }
    
    if len(validTerms) > 0 {
        // Return partial configuration
        return &parser.RebarConfig{
            Terms: validTerms,
            Raw:   content,
        }, errors
    }
    
    return nil, errors
}

Configuration Transformation

Configuration Migration

Migrate configurations between different formats or versions:

go
type ConfigMigrator struct {
    migrations []Migration
}

type Migration interface {
    Name() string
    Description() string
    Apply(config *parser.RebarConfig) (*parser.RebarConfig, error)
    CanApply(config *parser.RebarConfig) bool
}

// Example migration: Convert old-style dependencies to new format
type DependencyFormatMigration struct{}

func (m DependencyFormatMigration) Name() string {
    return "dependency-format-v2"
}

func (m DependencyFormatMigration) Description() string {
    return "Convert old-style dependency format to new format"
}

func (m DependencyFormatMigration) CanApply(config *parser.RebarConfig) bool {
    // Check if config uses old format
    if deps, ok := config.GetDeps(); ok && len(deps) > 0 {
        if depsList, ok := deps[0].(parser.List); ok {
            for _, dep := range depsList.Elements {
                // Check for old format patterns
                if atom, ok := dep.(parser.Atom); ok {
                    // Old format: just atom names
                    _ = atom
                    return true
                }
            }
        }
    }
    return false
}

func (m DependencyFormatMigration) Apply(config *parser.RebarConfig) (*parser.RebarConfig, error) {
    newTerms := make([]parser.Term, len(config.Terms))
    copy(newTerms, config.Terms)
    
    // Find and transform dependency terms
    for i, term := range newTerms {
        if tuple, ok := term.(parser.Tuple); ok && len(tuple.Elements) >= 2 {
            if atom, ok := tuple.Elements[0].(parser.Atom); ok && atom.Value == "deps" {
                newDeps, err := m.transformDependencies(tuple.Elements[1])
                if err != nil {
                    return nil, err
                }
                
                newTerms[i] = parser.Tuple{
                    Elements: []parser.Term{
                        tuple.Elements[0],
                        newDeps,
                    },
                }
            }
        }
    }
    
    return &parser.RebarConfig{
        Terms: newTerms,
        Raw:   "", // Raw will be regenerated when formatted
    }, nil
}

func (m DependencyFormatMigration) transformDependencies(deps parser.Term) (parser.Term, error) {
    if depsList, ok := deps.(parser.List); ok {
        var newElements []parser.Term
        
        for _, dep := range depsList.Elements {
            if atom, ok := dep.(parser.Atom); ok {
                // Convert atom to {atom, "latest"} tuple
                newDep := parser.Tuple{
                    Elements: []parser.Term{
                        atom,
                        parser.String{Value: "latest"},
                    },
                }
                newElements = append(newElements, newDep)
            } else {
                // Keep existing format
                newElements = append(newElements, dep)
            }
        }
        
        return parser.List{Elements: newElements}, nil
    }
    
    return deps, nil
}

func (cm *ConfigMigrator) Migrate(config *parser.RebarConfig) (*parser.RebarConfig, error) {
    current := config
    
    for _, migration := range cm.migrations {
        if migration.CanApply(current) {
            log.Printf("Applying migration: %s", migration.Name())
            
            migrated, err := migration.Apply(current)
            if err != nil {
                return nil, fmt.Errorf("migration %s failed: %w", migration.Name(), err)
            }
            
            current = migrated
        }
    }
    
    return current, nil
}

Configuration Merging

Merge multiple configurations intelligently:

go
type ConfigMerger struct {
    strategy MergeStrategy
}

type MergeStrategy interface {
    Merge(base, overlay *parser.RebarConfig) (*parser.RebarConfig, error)
}

// Strategy: Override - overlay completely replaces base sections
type OverrideStrategy struct{}

func (s OverrideStrategy) Merge(base, overlay *parser.RebarConfig) (*parser.RebarConfig, error) {
    baseTerms := createTermMap(base)
    overlayTerms := createTermMap(overlay)
    
    // Start with base terms
    result := make(map[string]parser.Term)
    for key, term := range baseTerms {
        result[key] = term
    }
    
    // Override with overlay terms
    for key, term := range overlayTerms {
        result[key] = term
    }
    
    // Convert back to slice
    var terms []parser.Term
    for _, term := range result {
        terms = append(terms, term)
    }
    
    return &parser.RebarConfig{Terms: terms}, nil
}

// Strategy: Merge - intelligently merge compatible sections
type MergeStrategy struct{}

func (s MergeStrategy) Merge(base, overlay *parser.RebarConfig) (*parser.RebarConfig, error) {
    baseTerms := createTermMap(base)
    overlayTerms := createTermMap(overlay)
    
    result := make(map[string]parser.Term)
    
    // Copy base terms
    for key, term := range baseTerms {
        result[key] = term
    }
    
    // Merge overlay terms
    for key, overlayTerm := range overlayTerms {
        if baseTerm, exists := result[key]; exists {
            // Try to merge compatible terms
            merged, err := s.mergeTerms(baseTerm, overlayTerm)
            if err != nil {
                // If merging fails, use overlay term
                result[key] = overlayTerm
            } else {
                result[key] = merged
            }
        } else {
            result[key] = overlayTerm
        }
    }
    
    var terms []parser.Term
    for _, term := range result {
        terms = append(terms, term)
    }
    
    return &parser.RebarConfig{Terms: terms}, nil
}

func (s MergeStrategy) mergeTerms(base, overlay parser.Term) (parser.Term, error) {
    // Only merge if both are tuples with list values (like deps, erl_opts)
    baseTuple, baseOk := base.(parser.Tuple)
    overlayTuple, overlayOk := overlay.(parser.Tuple)
    
    if !baseOk || !overlayOk || len(baseTuple.Elements) < 2 || len(overlayTuple.Elements) < 2 {
        return overlay, nil // Use overlay as-is
    }
    
    baseList, baseListOk := baseTuple.Elements[1].(parser.List)
    overlayList, overlayListOk := overlayTuple.Elements[1].(parser.List)
    
    if !baseListOk || !overlayListOk {
        return overlay, nil // Use overlay as-is
    }
    
    // Merge lists (remove duplicates)
    merged := append([]parser.Term{}, baseList.Elements...)
    
    for _, overlayElement := range overlayList.Elements {
        if !containsTerm(merged, overlayElement) {
            merged = append(merged, overlayElement)
        }
    }
    
    return parser.Tuple{
        Elements: []parser.Term{
            baseTuple.Elements[0], // Keep the key
            parser.List{Elements: merged},
        },
    }, nil
}

func containsTerm(terms []parser.Term, target parser.Term) bool {
    for _, term := range terms {
        if term.Compare(target) {
            return true
        }
    }
    return false
}

func createTermMap(config *parser.RebarConfig) map[string]parser.Term {
    termMap := make(map[string]parser.Term)
    
    for _, term := range config.Terms {
        if tuple, ok := term.(parser.Tuple); ok && len(tuple.Elements) > 0 {
            if atom, ok := tuple.Elements[0].(parser.Atom); ok {
                termMap[atom.Value] = term
            }
        }
    }
    
    return termMap
}

Plugin System

Extensible Analysis Framework

Create a plugin system for custom analysis:

go
type AnalysisPlugin interface {
    Name() string
    Description() string
    Analyze(config *parser.RebarConfig) (AnalysisResult, error)
}

type AnalysisResult struct {
    PluginName string
    Data       interface{}
    Issues     []Issue
}

type Issue struct {
    Severity string
    Message  string
    Location string
}

// Example plugin: Security analyzer
type SecurityAnalyzer struct{}

func (p SecurityAnalyzer) Name() string {
    return "security"
}

func (p SecurityAnalyzer) Description() string {
    return "Analyzes configuration for security issues"
}

func (p SecurityAnalyzer) Analyze(config *parser.RebarConfig) (AnalysisResult, error) {
    var issues []Issue
    
    // Check for insecure dependencies
    if deps, ok := config.GetDeps(); ok && len(deps) > 0 {
        if depsList, ok := deps[0].(parser.List); ok {
            for _, dep := range depsList.Elements {
                if tuple, ok := dep.(parser.Tuple); ok && len(tuple.Elements) >= 2 {
                    if name, ok := tuple.Elements[0].(parser.Atom); ok {
                        // Check for known insecure packages
                        if p.isInsecurePackage(name.Value) {
                            issues = append(issues, Issue{
                                Severity: "high",
                                Message:  fmt.Sprintf("Dependency '%s' has known security vulnerabilities", name.Value),
                                Location: "deps",
                            })
                        }
                        
                        // Check for git dependencies without pinned versions
                        if len(tuple.Elements) >= 2 {
                            if gitSpec, ok := tuple.Elements[1].(parser.Tuple); ok {
                                if len(gitSpec.Elements) > 0 {
                                    if source, ok := gitSpec.Elements[0].(parser.Atom); ok && source.Value == "git" {
                                        // Check if version is pinned
                                        if !p.hasVersionPin(gitSpec) {
                                            issues = append(issues, Issue{
                                                Severity: "medium",
                                                Message:  fmt.Sprintf("Git dependency '%s' should be pinned to a specific version", name.Value),
                                                Location: "deps",
                                            })
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    
    return AnalysisResult{
        PluginName: p.Name(),
        Data:       map[string]int{"total_issues": len(issues)},
        Issues:     issues,
    }, nil
}

func (p SecurityAnalyzer) isInsecurePackage(name string) bool {
    // This would typically check against a database of known vulnerabilities
    insecurePackages := []string{"vulnerable_package", "old_crypto_lib"}
    for _, pkg := range insecurePackages {
        if name == pkg {
            return true
        }
    }
    return false
}

func (p SecurityAnalyzer) hasVersionPin(gitSpec parser.Tuple) bool {
    // Check if git spec has a tag or specific commit
    for _, element := range gitSpec.Elements {
        if tuple, ok := element.(parser.Tuple); ok && len(tuple.Elements) >= 2 {
            if key, ok := tuple.Elements[0].(parser.Atom); ok {
                if key.Value == "tag" || key.Value == "ref" {
                    return true
                }
            }
        }
    }
    return false
}

// Plugin manager
type AnalysisManager struct {
    plugins []AnalysisPlugin
}

func NewAnalysisManager() *AnalysisManager {
    return &AnalysisManager{
        plugins: []AnalysisPlugin{
            SecurityAnalyzer{},
            // Add more plugins here
        },
    }
}

func (am *AnalysisManager) RunAnalysis(config *parser.RebarConfig) ([]AnalysisResult, error) {
    var results []AnalysisResult
    
    for _, plugin := range am.plugins {
        result, err := plugin.Analyze(config)
        if err != nil {
            log.Printf("Plugin %s failed: %v", plugin.Name(), err)
            continue
        }
        results = append(results, result)
    }
    
    return results, nil
}

func (am *AnalysisManager) GenerateReport(results []AnalysisResult) string {
    var report strings.Builder
    
    report.WriteString("=== Analysis Report ===\n\n")
    
    totalIssues := 0
    for _, result := range results {
        totalIssues += len(result.Issues)
    }
    
    report.WriteString(fmt.Sprintf("Total issues found: %d\n\n", totalIssues))
    
    for _, result := range results {
        report.WriteString(fmt.Sprintf("--- %s ---\n", strings.ToUpper(result.PluginName)))
        
        if len(result.Issues) == 0 {
            report.WriteString("✓ No issues found\n\n")
            continue
        }
        
        for _, issue := range result.Issues {
            severity := strings.ToUpper(issue.Severity)
            report.WriteString(fmt.Sprintf("[%s] %s\n", severity, issue.Message))
            if issue.Location != "" {
                report.WriteString(fmt.Sprintf("    Location: %s\n", issue.Location))
            }
        }
        report.WriteString("\n")
    }
    
    return report.String()
}

Best Practices Summary

1. Memory Management

  • Process large files one at a time
  • Use goroutines for concurrent processing
  • Allow garbage collection between operations

2. Error Handling

  • Create custom error types for better context
  • Implement recovery strategies for partial failures
  • Provide detailed error messages with location information

3. Configuration Management

  • Use migration patterns for format changes
  • Implement intelligent merging strategies
  • Validate configurations before processing

4. Extensibility

  • Design plugin systems for custom analysis
  • Use interfaces for flexible implementations
  • Provide comprehensive reporting mechanisms

5. Performance

  • Cache parsed configurations when possible
  • Use streaming for very large files
  • Profile memory usage in production

This advanced guide provides the foundation for building robust, production-ready applications using the Erlang Rebar Config Parser.

Released under the MIT License.