diff --git a/internal/meta/normalize.go b/internal/meta/normalize.go index fdd2454b3..3b9d01fd7 100644 --- a/internal/meta/normalize.go +++ b/internal/meta/normalize.go @@ -113,7 +113,8 @@ type EnumOption struct { } // EnumOptions returns the field's allowed values paired with their descriptions -// — from enum, or from options when enum is absent — coerced to the canonical +// — from enum (with descriptions backfilled from options when the field carries +// both forms), or from options when enum is absent — coerced to the canonical // type and ordered: numeric and boolean values are sorted; string values keep // source order (which can encode priority). Uncoercible literals are dropped. // Returns nil when the field declares no enum constraint. @@ -122,9 +123,14 @@ func (f Field) EnumOptions() []EnumOption { var out []EnumOption switch { case len(f.Enum) > 0: + // key by raw literal so enum "1" and option 1 align across JSON types + desc := make(map[string]string, len(f.Options)) + for _, o := range f.Options { + desc[fmt.Sprintf("%v", o.Value)] = o.Description + } for _, e := range f.Enum { if v, ok := coerceLiteral(ct, e); ok { - out = append(out, EnumOption{Value: v}) + out = append(out, EnumOption{Value: v, Description: desc[fmt.Sprintf("%v", e)]}) } } case len(f.Options) > 0: diff --git a/internal/meta/normalize_test.go b/internal/meta/normalize_test.go index 93e35ca64..45b12d935 100644 --- a/internal/meta/normalize_test.go +++ b/internal/meta/normalize_test.go @@ -80,6 +80,39 @@ func TestField_EnumOptions(t *testing.T) { } } +func TestField_EnumOptions_BothEnumAndOptions(t *testing.T) { + // enum is the value set; descriptions backfilled from options, empty where absent + f := Field{Type: "string", Enum: []any{"1", "2", "3", "4", "6"}, Options: []Option{ + {Value: "1", Description: "from"}, + {Value: "2", Description: "to"}, + {Value: "6", Description: "subject"}, + }} + want := []EnumOption{ + {Value: "1", Description: "from"}, + {Value: "2", Description: "to"}, + {Value: "3", Description: ""}, + {Value: "4", Description: ""}, + {Value: "6", Description: "subject"}, + } + if got := f.EnumOptions(); !reflect.DeepEqual(got, want) { + t.Errorf("EnumOptions(enum+options) = %+v, want %+v", got, want) + } + + // enum values stored as strings match option values stored as numbers + fi := Field{Type: "integer", Enum: []any{"10", "2", "1"}, Options: []Option{ + {Value: 1, Description: "one"}, + {Value: 2, Description: "two"}, + }} + wantI := []EnumOption{ + {Value: int64(1), Description: "one"}, + {Value: int64(2), Description: "two"}, + {Value: int64(10), Description: ""}, + } + if got := fi.EnumOptions(); !reflect.DeepEqual(got, wantI) { + t.Errorf("EnumOptions(integer enum+options) = %+v, want %+v", got, wantI) + } +} + func TestField_Enum_NumberAndBoolean(t *testing.T) { // number: string-stored floats coerced to float64 and numerically sorted if got := (Field{Type: "number", Enum: []any{"2.5", "1.5", "10"}}).EnumValues(); !reflect.DeepEqual(got, []any{1.5, 2.5, float64(10)}) { diff --git a/internal/schema/assembler_test.go b/internal/schema/assembler_test.go index c7bd2a0c4..0465c41de 100644 --- a/internal/schema/assembler_test.go +++ b/internal/schema/assembler_test.go @@ -472,6 +472,18 @@ func TestConvert_EnumDescriptions(t *testing.T) { if bare.EnumDescriptions != nil { t.Errorf("bare enum must have nil EnumDescriptions, got %v", bare.EnumDescriptions) } + + // enum + options both present -> enumDescriptions backfilled, aligned, "" where absent + both := Convert(meta.Field{Type: "string", Enum: []any{"1", "2", "3"}, Options: []meta.Option{ + {Value: "1", Description: "from"}, + {Value: "2", Description: "to"}, + }}) + if !reflect.DeepEqual(both.Enum, []interface{}{"1", "2", "3"}) { + t.Errorf("both Enum = %v", both.Enum) + } + if !reflect.DeepEqual(both.EnumDescriptions, []string{"from", "to", ""}) { + t.Errorf("both EnumDescriptions = %v, want [from to \"\"] aligned with enum", both.EnumDescriptions) + } } func TestBuildMeta_AffordanceFromMethod(t *testing.T) {