Skip to content

feat(mock): Add map field support in mock generator #34

@SebastienMelki

Description

@SebastienMelki

Feature Description

The mock generator needs to handle protobuf map fields (map<K, V>) which are currently not implemented.

Current State

Map fields are not explicitly handled in the generateMockFieldAssignments switch statement, likely falling through to the default TODO case or being treated as regular message fields.

Background

In protobuf, maps are syntactic sugar for repeated message fields with special semantics:

map<string, string> metadata = 1;
// Is equivalent to:
message MetadataEntry {
  string key = 1;
  string value = 2;
}
repeated MetadataEntry metadata = 1;

Proposed Solution

1. Detect Map Fields

Add map field detection and generation:

case protoreflect.MessageKind:
    if field.Desc.IsMap() {
        g.generateMapField(gf, field, varName, fieldPath)
    } else if field.Desc.IsList() {
        // Existing repeated message logic
    } else {
        // Existing single message logic
    }

2. Implement Map Generation

func (g *Generator) generateMapField(
    gf *protogen.GeneratedFile,
    field *protogen.Field,
    varName string,
    fieldPath string,
) {
    fieldName := field.GoName
    keyKind := field.Message.Fields[0].Desc.Kind()
    valueKind := field.Message.Fields[1].Desc.Kind()
    
    // Generate map initialization
    gf.P(varName, ".", fieldName, " = map[", 
        g.getGoType(field.Message.Fields[0]), "][",
        g.getGoType(field.Message.Fields[1]), "]{")
    
    // Generate 2-3 entries by default
    for i := 0; i < defaultMapSize; i++ {
        key := g.generateMapKey(field.Message.Fields[0], i)
        value := g.generateMapValue(field.Message.Fields[1], fieldPath, i)
        gf.P(key, ": ", value, ",")
    }
    
    gf.P("}")
}

3. Key Generation

func (g *Generator) generateMapKey(keyField *protogen.Field, index int) string {
    switch keyField.Desc.Kind() {
    case protoreflect.StringKind:
        return fmt.Sprintf(`"key_%d"`, index)
    case protoreflect.Int32Kind, protoreflect.Int64Kind:
        return fmt.Sprintf("%d", index)
    case protoreflect.Uint32Kind, protoreflect.Uint64Kind:
        return fmt.Sprintf("%d", index)
    case protoreflect.BoolKind:
        return fmt.Sprintf("%t", index%2 == 0)
    default:
        return `"key"`
    }
}

4. Value Generation

func (g *Generator) generateMapValue(
    valueField *protogen.Field,
    fieldPath string,
    index int,
) string {
    valuePath := fmt.Sprintf("%s[%d]", fieldPath, index)
    
    switch valueField.Desc.Kind() {
    case protoreflect.StringKind:
        return fmt.Sprintf(`selectStringExample("%s", generateString)`, valuePath)
    case protoreflect.MessageKind:
        // For message values, generate inline
        return fmt.Sprintf("&%s{/* populated separately */}", valueField.Message.GoIdent)
    case protoreflect.Int32Kind, protoreflect.Int64Kind:
        return fmt.Sprintf("selectIntExample(\"%s\", %d)", valuePath, 42+index)
    // ... other types
    default:
        return g.getDefaultValue(valueField)
    }
}

5. Complex Map Values

For maps with message values, populate after initialization:

if valueKind == protoreflect.MessageKind {
    // After map initialization, populate message values
    for i := 0; i < defaultMapSize; i++ {
        key := g.generateMapKey(field.Message.Fields[0], i)
        valueVar := fmt.Sprintf("%s.%s[%s]", varName, fieldName, key)
        g.generateMockFieldAssignments(gf, field.Message.Fields[1].Message, valueVar)
    }
}

Common Map Patterns

Support common map patterns with smart defaults:

func (g *Generator) getMapSize(field *protogen.Field) int {
    fieldName := strings.ToLower(string(field.Desc.Name()))
    
    switch {
    case strings.Contains(fieldName, "metadata"):
        return 3 // More entries for metadata
    case strings.Contains(fieldName, "headers"):
        return 4 // HTTP headers typically have several
    case strings.Contains(fieldName, "labels"):
        return 2 // Labels/tags
    default:
        return defaultMapSize
    }
}

Testing Requirements

Test Proto Files

message MapTestMessage {
  // Basic maps
  map<string, string> string_map = 1;
  map<int32, string> int_string_map = 2;
  map<string, int32> string_int_map = 3;
  
  // Complex maps
  map<string, NestedMessage> message_map = 4;
  map<int64, TestEnum> enum_map = 5;
  
  // Edge cases
  map<bool, bool> bool_map = 6;
  map<uint64, bytes> bytes_map = 7;
}

Test Cases

  • Maps with all key types (string, int32, int64, uint32, uint64, bool)
  • Maps with all value types (scalars, messages, enums)
  • Empty maps (when configured)
  • Large maps (10+ entries)
  • Nested maps (message containing maps in map values)

Files Affected

  • internal/httpgen/mock_generator.go - Add map detection and generation
  • internal/httpgen/testdata/proto/test_maps.proto - Map test cases
  • internal/httpgen/testdata/golden/*_http_mock.pb.go - Golden files

Success Criteria

  • All valid map key types supported
  • All value types supported (scalar, enum, message)
  • Generated maps have appropriate number of entries
  • Map keys are unique within each map
  • Generated code compiles without errors
  • Message values in maps are properly populated

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions