Files
masonry/temp_multi-output-template-plan.md

10 KiB

Looking at your current TemplateInterpreter design, you'll need to extend it to handle multi-file output scenarios. Here are several strategies to consider:

1. Multi-Output Template Registry

Extend your template registry to support templates that define multiple output files:

type MultiFileTemplate struct {
    Name        string
    OutputPath  string
    Content     string
    Dependencies []string
}

type MultiFileRegistry struct {
    templates map[string][]MultiFileTemplate
    funcMap   template.FuncMap
}

2. Template Manifest Approach

Create a manifest system where templates can declare their output structure:

type TemplateManifest struct {
    Name        string `yaml:"name"`
    Description string `yaml:"description"`
    Outputs     []OutputFile `yaml:"outputs"`
}

type OutputFile struct {
    Path     string `yaml:"path"`
    Template string `yaml:"template"`
    Condition string `yaml:"condition,omitempty"`
}

3. Directory-Based Output Strategy

Modify your interpreter to return a map of file paths to content:

func (ti *TemplateInterpreter) InterpretMultiFile(masonryInput, templateDir string) (map[string]string, error) {
    // Returns map[outputPath]content
}

4. Template Composition with File Directives

Add special template functions for file operations:

// In your funcMap
"writeFile": func(path, content string) string {
    // Queue file for writing
    return ""
},
"includeFile": func(templateName string, data interface{}) string {
    // Include another template's output
},

5. Build Pipeline Approach

Create a build pipeline that processes templates in stages:

type BuildPipeline struct {
    stages []BuildStage
}

type BuildStage struct {
    Name      string
    Templates []string
    OutputDir string
    Condition func(ast lang.AST) bool
}

I'd suggest starting with a Template Manifest + Multi-Output approach. This gives you flexibility while maintaining the simplicity of your current design:

type MultiFileInterpreter struct {
    registry *TemplateRegistry
    outputs  map[string]string
}

func (mfi *MultiFileInterpreter) InterpretToFiles(masonryInput, templateDir, manifestFile string) (map[string]string, error) {
    // Load manifest
    // Process each output file defined in manifest
    // Return map of filepath -> content
}

This approach allows you to:

  • Keep your existing single-file templates working
  • Define complex multi-file outputs via manifest files
  • Maintain template reusability across different output scenarios
  • Support conditional file generation based on AST content

Would you like me to elaborate on any of these strategies or help implement a specific approach?

Yeah, can you show me how a Template Manifest with Multi-output might work? I'd like to also see what it would look like for an end user to set up and get working.

I'll show you how a Template Manifest with Multi-output approach would work, including the implementation and user experience.

Implementation

First, let's define the manifest structure:

type TemplateManifest struct {
    Name        string       `yaml:"name"`
    Description string       `yaml:"description"`
    Outputs     []OutputFile `yaml:"outputs"`
}

type OutputFile struct {
    Path      string `yaml:"path"`
    Template  string `yaml:"template"`
    Condition string `yaml:"condition,omitempty"`
}

Now let's extend your TemplateInterpreter to support multi-file output:

// InterpretToFiles processes a manifest and returns multiple output files
func (ti *TemplateInterpreter) InterpretToFiles(masonryFile, templateDir, manifestFile string) (map[string]string, error) {
    // Load templates from directory
    err := ti.registry.LoadFromDirectory(templateDir)
    if err != nil {
        return nil, fmt.Errorf("error loading templates: %w", err)
    }

    // Load manifest
    manifestPath := filepath.Join(templateDir, manifestFile)
    manifestData, err := os.ReadFile(manifestPath)
    if err != nil {
        return nil, fmt.Errorf("error reading manifest: %w", err)
    }

    var manifest TemplateManifest
    err = yaml.Unmarshal(manifestData, &manifest)
    if err != nil {
        return nil, fmt.Errorf("error parsing manifest: %w", err)
    }

    // Parse Masonry input
    masonryInput, err := os.ReadFile(masonryFile)
    if err != nil {
        return nil, fmt.Errorf("error reading Masonry file: %w", err)
    }

    ast, err := lang.ParseInput(string(masonryInput))
    if err != nil {
        return nil, fmt.Errorf("error parsing Masonry input: %w", err)
    }

    // Process each output file
    outputs := make(map[string]string)
    for _, output := range manifest.Outputs {
        // Check condition if specified
        if output.Condition != "" && !ti.evaluateCondition(output.Condition, ast) {
            continue
        }

        // Execute template
        content, err := ti.executeTemplate(output.Template, ast)
        if err != nil {
            return nil, fmt.Errorf("error executing template %s: %w", output.Template, err)
        }

        outputs[output.Path] = content
    }

    return outputs, nil
}

func (ti *TemplateInterpreter) executeTemplate(templateName string, ast lang.AST) (string, error) {
    if tmpl, exists := ti.registry.templates[templateName]; exists {
        data := struct {
            AST      lang.AST
            Registry *TemplateRegistry
        }{
            AST:      ast,
            Registry: ti.registry,
        }

        var buf bytes.Buffer
        err := tmpl.Execute(&buf, data)
        return buf.String(), err
    }
    return "", fmt.Errorf("template %s not found", templateName)
}

func (ti *TemplateInterpreter) evaluateCondition(condition string, ast lang.AST) bool {
    // Simple condition evaluation - you can expand this
    switch condition {
    case "has_api":
        return len(ast.APIs) > 0
    case "has_ui":
        return len(ast.UIs) > 0
    case "has_database":
        return len(ast.Databases) > 0
    default:
        return true
    }
}

You'll also need to add the yaml import:

import "gopkg.in/yaml.v3"

End User Experience

Here's how an end user would set up a React component generator:

1. Project Structure

templates/react-components/
├── manifest.yaml
├── component.tmpl
├── component-test.tmpl
├── index.tmpl
└── styles.tmpl

2. Manifest File (manifest.yaml)

name: "React Component Generator"
description: "Generates React components with tests and styles"
outputs:
  - path: "src/components/{{.AST.UIs.0.Name}}/{{.AST.UIs.0.Name}}.tsx"
    template: "component"
    condition: "has_ui"
  - path: "src/components/{{.AST.UIs.0.Name}}/{{.AST.UIs.0.Name}}.test.tsx"
    template: "component-test"
    condition: "has_ui"
  - path: "src/components/{{.AST.UIs.0.Name}}/index.ts"
    template: "index"
    condition: "has_ui"
  - path: "src/components/{{.AST.UIs.0.Name}}/{{.AST.UIs.0.Name}}.module.css"
    template: "styles"
    condition: "has_ui"

3. Template Files

component.tmpl:

import React from 'react';
import styles from './{{.AST.UIs.0.Name}}.module.css';

interface {{.AST.UIs.0.Name}}Props {
{{- range .AST.UIs.0.Fields}}
  {{.Name}}: {{goType .Type}};
{{- end}}
}

export const {{.AST.UIs.0.Name}}: React.FC<{{.AST.UIs.0.Name}}Props> = ({
{{- range $i, $field := .AST.UIs.0.Fields}}
  {{if $i}},{{end}} {{$field.Name}}
{{- end}}
}) => {
  return (
    <div className={styles.container}>
      <h2>{{.AST.UIs.0.Name}}</h2>
{{- range .AST.UIs.0.Fields}}
      <div>{{.Name}}: {{{.Name}}}</div>
{{- end}}
    </div>
  );
};

component-test.tmpl:

import { render, screen } from '@testing-library/react';
import { {{.AST.UIs.0.Name}} } from './{{.AST.UIs.0.Name}}';

describe('{{.AST.UIs.0.Name}}', () => {
  it('renders without crashing', () => {
    render(<{{.AST.UIs.0.Name}}{{range .AST.UIs.0.Fields}} {{.Name}}="test"{{end}} />);
    expect(screen.getByText('{{.AST.UIs.0.Name}}')).toBeInTheDocument();
  });
});

4. Masonry Input (user-profile.masonry)

ui UserProfile {
    field name: string
    field email: string
    field age: int
}

5. CLI Usage

# Build the CLI
go build -o masonry.exe ./cmd/cli/

# Generate files using manifest
./masonry.exe generate --input user-profile.masonry --template-dir templates/react-components --manifest manifest.yaml --output-dir ./generated

6. CLI Command Implementation

Add this to your CLI:

func generateMultiFileCommand() *cobra.Command {
    var inputFile, templateDir, manifestFile, outputDir string
    
    cmd := &cobra.Command{
        Use:   "generate-multi",
        Short: "Generate multiple files using a template manifest",
        RunE: func(cmd *cobra.Command, args []string) error {
            interpreter := interpreter.NewTemplateInterpreter()
            
            outputs, err := interpreter.InterpretToFiles(inputFile, templateDir, manifestFile)
            if err != nil {
                return err
            }
            
            // Write all output files
            for filePath, content := range outputs {
                fullPath := filepath.Join(outputDir, filePath)
                
                // Create directory if it doesn't exist
                dir := filepath.Dir(fullPath)
                err := os.MkdirAll(dir, 0755)
                if err != nil {
                    return fmt.Errorf("error creating directory %s: %w", dir, err)
                }
                
                // Write file
                err = os.WriteFile(fullPath, []byte(content), 0644)
                if err != nil {
                    return fmt.Errorf("error writing file %s: %w", fullPath, err)
                }
                
                fmt.Printf("Generated: %s\n", fullPath)
            }
            
            return nil
        },
    }
    
    cmd.Flags().StringVarP(&inputFile, "input", "i", "", "Input Masonry file")
    cmd.Flags().StringVarP(&templateDir, "template-dir", "t", "", "Template directory")
    cmd.Flags().StringVarP(&manifestFile, "manifest", "m", "manifest.yaml", "Manifest file name")
    cmd.Flags().StringVarP(&outputDir, "output-dir", "o", "./generated", "Output directory")
    
    cmd.MarkFlagRequired("input")
    cmd.MarkFlagRequired("template-dir")
    
    return cmd
}

This approach gives users a clean, declarative way to define complex multi-file outputs while maintaining the flexibility of your template system. The manifest acts as a blueprint that maps Masonry definitions to specific output files with conditional generation based on the AST content.