Skip to content

refactor(mock): Consolidate field type handling into reusable pattern #35

@SebastienMelki

Description

@SebastienMelki

Description

After implementing support for all field types, refactor the mock generator to use a more maintainable and extensible pattern for field type handling.

Current Problem

The generateMockFieldAssignments method will become very large with many similar patterns repeated across different field types. This makes it:

  • Hard to maintain
  • Prone to inconsistencies
  • Difficult to extend with new features

Proposed Refactoring

1. Field Handler Interface

Create a consistent interface for field generation:

type FieldHandler interface {
    CanHandle(field *protogen.Field) bool
    Generate(g *Generator, gf *protogen.GeneratedFile, field *protogen.Field, varName string, depth int)
}

type FieldHandlerRegistry struct {
    handlers []FieldHandler
}

func (r *FieldHandlerRegistry) GetHandler(field *protogen.Field) FieldHandler {
    for _, h := range r.handlers {
        if h.CanHandle(field) {
            return h
        }
    }
    return &DefaultFieldHandler{}
}

2. Specialized Handlers

type StringFieldHandler struct{}

func (h *StringFieldHandler) CanHandle(field *protogen.Field) bool {
    return field.Desc.Kind() == protoreflect.StringKind
}

func (h *StringFieldHandler) Generate(
    g *Generator,
    gf *protogen.GeneratedFile,
    field *protogen.Field,
    varName string,
    depth int,
) {
    fieldName := field.GoName
    fieldPath := g.getFieldPath(field)
    
    if field.Desc.IsList() {
        g.generateRepeatedField(gf, field, varName, func(i int) string {
            return fmt.Sprintf(`selectStringExample("%s", %s)`, 
                fieldPath, g.getDefaultGenerator(field))
        })
    } else {
        gf.P(varName, ".", fieldName, ` = selectStringExample("`, 
            fieldPath, `", `, g.getDefaultGenerator(field), `)`)
    }
}

3. Shared Generation Utilities

func (g *Generator) generateRepeatedField(
    gf *protogen.GeneratedFile,
    field *protogen.Field,
    varName string,
    valueGenerator func(index int) string,
) {
    fieldName := field.GoName
    fieldType := g.getFieldType(field)
    
    gf.P(varName, ".", fieldName, " = []", fieldType, "{")
    for i := 0; i < g.config.ListSize; i++ {
        gf.P(valueGenerator(i), ",")
    }
    gf.P("}")
}

func (g *Generator) generateMapField(
    gf *protogen.GeneratedFile,
    field *protogen.Field,
    varName string,
    keyGen func(int) string,
    valueGen func(int) string,
) {
    // Shared map generation logic
}

4. Configuration Object

type MockConfig struct {
    ListSize        int
    MapSize         int
    MaxDepth        int
    SkipOptionalAt  int
    IncludeExamples bool
    SmartDefaults   bool
}

func (g *Generator) Configure(cfg MockConfig) {
    g.config = cfg
}

5. Simplified Main Method

func (g *Generator) generateMockFieldAssignments(
    gf *protogen.GeneratedFile,
    message *protogen.Message,
    varName string,
) {
    g.generateMockFieldAssignmentsWithDepth(gf, message, varName, 0)
}

func (g *Generator) generateMockFieldAssignmentsWithDepth(
    gf *protogen.GeneratedFile,
    message *protogen.Message,
    varName string,
    depth int,
) {
    if depth >= g.config.MaxDepth {
        return
    }
    
    for _, field := range message.Fields {
        if !g.shouldGenerateField(field, depth) {
            continue
        }
        
        handler := g.registry.GetHandler(field)
        handler.Generate(g, gf, field, varName, depth)
    }
}

Benefits

Maintainability

  • Each field type handler is isolated and testable
  • Common patterns are extracted to utilities
  • Easy to find and fix type-specific bugs

Extensibility

  • New field types just need a new handler
  • Custom behavior can be added via handler composition
  • Plugin system for project-specific mock generation

Consistency

  • Shared utilities ensure consistent behavior
  • Configuration is centralized
  • Patterns are enforced through interfaces

Testing

  • Each handler can be unit tested independently
  • Mock the registry for testing specific scenarios
  • Easier to test edge cases per type

Migration Plan

  1. Phase 1: Implement the handler interface and registry
  2. Phase 2: Create handlers for existing supported types
  3. Phase 3: Migrate one type at a time to handlers
  4. Phase 4: Extract common utilities
  5. Phase 5: Remove old switch statement
  6. Phase 6: Add new types using handler pattern

Files Affected

  • internal/httpgen/mock_generator.go - Main refactoring
  • internal/httpgen/mock_handlers.go - New file for handlers
  • internal/httpgen/mock_config.go - New file for configuration
  • internal/httpgen/mock_utils.go - New file for utilities

Success Criteria

  • All existing functionality preserved
  • Code coverage maintained or improved
  • New field types can be added in < 50 lines
  • Performance not degraded
  • Generated output identical to before refactoring

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions