Files
lukaszraczylo 3c04d7b0b1 feat(api): typed enums for all string-enum fields
The Telegram docs describe many string fields and parameters with
phrases like "can be ..., or ...", "must be one of ...", or "always X",
yet the generated Go API surface used raw `string` for every one of
them. Callers had to write magic strings or `string(api.ChatTypePrivate)`
to satisfy the field type. This change makes those fields typed Go
string enums emitted from the IR, so the IDE autocompletes valid values
and breaking-value drift surfaces at compile time.

Pipeline changes:

- internal/spec/ir.go: Field gains EnumValues []string. Empty for non-
  enum fields; otherwise the wire-level values in doc order, deduped.

- cmd/scrape/enums.go: extractEnumValues recognises the curly-quoted
  patterns Telegram uses ("can be either", "currently can be", "one
  of", "must be", "always X") and rejects free-text quoted refs (e.g.
  "Can be available only for X") via a tight gap check between the
  trigger phrase and the first quoted value. parse_mode parameters
  get the canonical Markdown / MarkdownV2 / HTML triple injected
  because Telegram links to a separate formatting-options section
  instead of listing values inline.

- cmd/genapi/enums.go: planEnums groups fields by sorted value-tuple,
  picks a canonical Go enum name (most-common candidate, parent-
  prefixed beats plain, shortest beats longer, alphabetical for
  determinism), resolves cross-group name collisions by parent prefix.

- cmd/genapi/emitter.go + templates: goField rewrites the field type
  to the planned enum name; multipartFieldEntry casts typed enum
  values back to string when composing the wire map; enums.tmpl now
  iterates the planned enums instead of hardcoding four hand-curated
  ones; sentinelForField produces typed-constant test fixtures.

- api/enums.gen.go: regenerated from the live IR. 66 enum types, 155
  constants. ParseMode, ChatType, MessageEntityType, ChatMember /
  MessageOrigin / PaidMedia / Background / StoryAreaType / Reaction /
  TransactionPartner / PassportElement variant Status & Type fields
  are now typed.

- api/enums.go: hand-coded UpdateType (used by transport.LongPoller).
  The Telegram docs do not enumerate Update payload kinds inline, so
  the codegen pipeline cannot synthesise this enum.

- api/types.gen.go, api/methods.gen.go, api/methods_gen_test.go: 137
  field declarations rewritten string -> typed enum.

- dispatch/, examples/: dropped every string(api.<Const>) cast. The
  HasEntity filter now takes api.MessageEntityType; ChatType filter
  compares typed values directly. ChatMember discriminator filter
  casts variant.Status (typed per variant) to string for comparison.

- internal/spec/api.json, testdata/golden/*: regenerated and
  refreshed. make regen-from-fixture is byte-deterministic across
  runs.

Renames (no compat shims; v1 pre-public):
- EntityX  -> MessageEntityTypeX  (e.g. EntityBotCommand -> MessageEntityTypeBotCommand)
- EntityStrike -> MessageEntityTypeStrikethrough (full wire name)
2026-05-09 17:55:34 +01:00

646 lines
21 KiB
Go

package main
import (
"testing"
"github.com/lukaszraczylo/go-telegram/internal/spec"
"github.com/stretchr/testify/require"
)
// ---------------------------------------------------------------------------
// goType — all branches
// ---------------------------------------------------------------------------
func TestGoType_Primitive(t *testing.T) {
cases := []struct {
name string
optional bool
want string
}{
{"bool", false, "bool"},
{"bool", true, "*bool"},
{"int64", false, "int64"},
{"int64", true, "*int64"},
{"float64", false, "float64"},
{"float64", true, "*float64"},
{"string", false, "string"},
{"string", true, "string"}, // string is not pointer-wrapped
}
for _, c := range cases {
tr := spec.TypeRef{Kind: spec.KindPrimitive, Name: c.name}
got := goType(tr, c.optional)
require.Equal(t, c.want, got, "goType(%q, optional=%v)", c.name, c.optional)
}
}
func TestGoType_Named_Required(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindNamed, Name: "Message"}
require.Equal(t, "Message", goType(tr, false))
require.Equal(t, "*Message", goType(tr, true))
}
func TestGoType_Named_InputFile(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindNamed, Name: "InputFile"}
// InputFile is always pointer even when required.
require.Equal(t, "*InputFile", goType(tr, false))
require.Equal(t, "*InputFile", goType(tr, true))
}
func TestGoType_Named_UnionInterface(t *testing.T) {
// ChatMember is a known discriminated union — no * even when optional.
tr := spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}
require.Equal(t, "ChatMember", goType(tr, false))
require.Equal(t, "ChatMember", goType(tr, true))
}
func TestGoType_Array_NilElem(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindArray}
require.Equal(t, "[]any", goType(tr, false))
}
func TestGoType_Array_WithElem(t *testing.T) {
elem := spec.TypeRef{Kind: spec.KindNamed, Name: "Update"}
tr := spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
require.Equal(t, "[]Update", goType(tr, false))
}
func TestGoType_OneOf_ChatID(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"int64", "string"}}
require.Equal(t, "ChatID", goType(tr, false))
require.Equal(t, "*ChatID", goType(tr, true))
}
func TestGoType_OneOf_InputFile(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"InputFile", "string"}}
require.Equal(t, "*InputFile", goType(tr, false))
}
func TestGoType_OneOf_SealedInterface(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"A", "B"}}
require.Equal(t, "any", goType(tr, false))
}
func TestGoType_Unknown(t *testing.T) {
tr := spec.TypeRef{Kind: spec.Kind(99)}
require.Equal(t, "any", goType(tr, false))
}
// ---------------------------------------------------------------------------
// returnGoType — all branches
// ---------------------------------------------------------------------------
func TestReturnGoType_Primitive(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindPrimitive, Name: "bool"}
require.Equal(t, "bool", returnGoType(tr))
}
func TestReturnGoType_Named(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindNamed, Name: "Message"}
require.Equal(t, "*Message", returnGoType(tr))
}
func TestReturnGoType_Array_NilElem(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindArray}
require.Equal(t, "[]any", returnGoType(tr))
}
func TestReturnGoType_Array_WithElem(t *testing.T) {
elem := spec.TypeRef{Kind: spec.KindNamed, Name: "Update"}
tr := spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
require.Equal(t, "[]Update", returnGoType(tr))
}
func TestReturnGoType_OneOf_ChatID(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"int64", "string"}}
require.Equal(t, "ChatID", returnGoType(tr))
}
func TestReturnGoType_OneOf_Other(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"A", "B"}}
require.Equal(t, "any", returnGoType(tr))
}
func TestReturnGoType_Unknown(t *testing.T) {
tr := spec.TypeRef{Kind: spec.Kind(99)}
require.Equal(t, "any", returnGoType(tr))
}
// ---------------------------------------------------------------------------
// returnGoElem — all branches
// ---------------------------------------------------------------------------
func TestReturnGoElem_Primitive(t *testing.T) {
require.Equal(t, "int64", returnGoElem(spec.TypeRef{Kind: spec.KindPrimitive, Name: "int64"}))
}
func TestReturnGoElem_Named(t *testing.T) {
require.Equal(t, "Message", returnGoElem(spec.TypeRef{Kind: spec.KindNamed, Name: "Message"}))
}
func TestReturnGoElem_Array_NilElem(t *testing.T) {
require.Equal(t, "any", returnGoElem(spec.TypeRef{Kind: spec.KindArray}))
}
func TestReturnGoElem_Array_WithElem(t *testing.T) {
elem := spec.TypeRef{Kind: spec.KindNamed, Name: "PhotoSize"}
tr := spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
require.Equal(t, "[]PhotoSize", returnGoElem(tr))
}
func TestReturnGoElem_Unknown(t *testing.T) {
require.Equal(t, "any", returnGoElem(spec.TypeRef{Kind: spec.Kind(99)}))
}
// ---------------------------------------------------------------------------
// multipartFieldEntry — all branches
// ---------------------------------------------------------------------------
func makeField(name, jname, typName string, kind spec.Kind, required bool) spec.Field {
return spec.Field{
Name: name,
JSONName: jname,
Type: spec.TypeRef{Kind: kind, Name: typName},
Required: required,
}
}
func makeFieldVariants(name, jname string, kind spec.Kind, variants []string, required bool) spec.Field {
return spec.Field{
Name: name,
JSONName: jname,
Type: spec.TypeRef{Kind: kind, Variants: variants},
Required: required,
}
}
func TestMultipartFieldEntry_Int64Required(t *testing.T) {
f := makeField("ChatID", "chat_id", "int64", spec.KindPrimitive, true)
got := multipartFieldEntry(nil, "", f)
require.Contains(t, got, `FormatInt`)
require.NotContains(t, got, "if p.")
}
func TestMultipartFieldEntry_Int64Optional(t *testing.T) {
f := makeField("MessageThreadID", "message_thread_id", "int64", spec.KindPrimitive, false)
got := multipartFieldEntry(nil, "", f)
require.Contains(t, got, `FormatInt`)
require.Contains(t, got, "if p.")
}
func TestMultipartFieldEntry_StringRequired(t *testing.T) {
f := makeField("Text", "text", "string", spec.KindPrimitive, true)
got := multipartFieldEntry(nil, "", f)
require.Contains(t, got, `out["text"]`)
require.NotContains(t, got, "if p.Text")
}
func TestMultipartFieldEntry_StringOptional(t *testing.T) {
f := makeField("ParseMode", "parse_mode", "string", spec.KindPrimitive, false)
got := multipartFieldEntry(nil, "", f)
require.Contains(t, got, `if p.ParseMode`)
}
func TestMultipartFieldEntry_BoolRequired(t *testing.T) {
f := makeField("DisableNotification", "disable_notification", "bool", spec.KindPrimitive, true)
got := multipartFieldEntry(nil, "", f)
require.Contains(t, got, `FormatBool`)
require.NotContains(t, got, "if p.")
}
func TestMultipartFieldEntry_BoolOptional(t *testing.T) {
f := makeField("Protected", "protect_content", "bool", spec.KindPrimitive, false)
got := multipartFieldEntry(nil, "", f)
require.Contains(t, got, `FormatBool`)
require.Contains(t, got, "if p.")
}
func TestMultipartFieldEntry_Float64Required(t *testing.T) {
f := makeField("Latitude", "latitude", "float64", spec.KindPrimitive, true)
got := multipartFieldEntry(nil, "", f)
require.Contains(t, got, `FormatFloat`)
require.NotContains(t, got, "if p.")
}
func TestMultipartFieldEntry_Float64Optional(t *testing.T) {
f := makeField("Longitude", "longitude", "float64", spec.KindPrimitive, false)
got := multipartFieldEntry(nil, "", f)
require.Contains(t, got, `FormatFloat`)
require.Contains(t, got, "if p.")
}
func TestMultipartFieldEntry_OneOf_ChatIDRequired(t *testing.T) {
f := makeFieldVariants("ChatID", "chat_id", spec.KindOneOf, []string{"int64", "string"}, true)
got := multipartFieldEntry(nil, "", f)
require.Contains(t, got, `.String()`)
require.NotContains(t, got, "IsZero")
}
func TestMultipartFieldEntry_OneOf_ChatIDOptional(t *testing.T) {
f := makeFieldVariants("ChatID", "chat_id", spec.KindOneOf, []string{"int64", "string"}, false)
got := multipartFieldEntry(nil, "", f)
require.Contains(t, got, `IsZero`)
}
func TestMultipartFieldEntry_OneOf_InputFileOrString(t *testing.T) {
f := makeFieldVariants("Photo", "photo", spec.KindOneOf, []string{"InputFile", "string"}, false)
got := multipartFieldEntry(nil, "", f)
require.Contains(t, got, `PathOrID`)
}
func TestMultipartFieldEntry_OneOf_SealedRequired(t *testing.T) {
f := makeFieldVariants("Markup", "reply_markup", spec.KindOneOf, []string{"A", "B"}, true)
got := multipartFieldEntry(nil, "", f)
require.Contains(t, got, `json.Marshal`)
}
func TestMultipartFieldEntry_OneOf_SealedOptional(t *testing.T) {
f := makeFieldVariants("Markup", "reply_markup", spec.KindOneOf, []string{"A", "B"}, false)
got := multipartFieldEntry(nil, "", f)
require.Contains(t, got, `json.Marshal`)
require.Contains(t, got, "if p.Markup")
}
func TestMultipartFieldEntry_Named_Required(t *testing.T) {
f := makeField("Entities", "entities", "MessageEntity", spec.KindNamed, true)
got := multipartFieldEntry(nil, "", f)
require.Contains(t, got, `json.Marshal`)
require.NotContains(t, got, "if p.")
}
func TestMultipartFieldEntry_Named_Optional(t *testing.T) {
f := makeField("Entities", "entities", "MessageEntity", spec.KindNamed, false)
got := multipartFieldEntry(nil, "", f)
require.Contains(t, got, `json.Marshal`)
require.Contains(t, got, "if p.")
}
// ---------------------------------------------------------------------------
// unionTypeFor — all branches
// ---------------------------------------------------------------------------
func TestUnionTypeFor_DirectNamed(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}
name, ok := unionTypeFor(tr)
require.True(t, ok)
require.Equal(t, "ChatMember", name)
}
func TestUnionTypeFor_Array(t *testing.T) {
elem := spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}
tr := spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
name, ok := unionTypeFor(tr)
require.True(t, ok)
require.Equal(t, "ChatMember", name)
}
func TestUnionTypeFor_ArrayNilElem(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindArray}
_, ok := unionTypeFor(tr)
require.False(t, ok)
}
func TestUnionTypeFor_NotUnion(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindNamed, Name: "Message"}
_, ok := unionTypeFor(tr)
require.False(t, ok)
}
func TestUnionTypeFor_Unknown(t *testing.T) {
tr := spec.TypeRef{Kind: spec.Kind(99)}
_, ok := unionTypeFor(tr)
require.False(t, ok)
}
// ---------------------------------------------------------------------------
// unionNameByVariants
// ---------------------------------------------------------------------------
func TestUnionNameByVariants_ChatMember(t *testing.T) {
// Use the actual variants from knownDiscriminators["ChatMember"].
variants := []string{
"ChatMemberOwner", "ChatMemberAdministrator", "ChatMemberMember",
"ChatMemberRestricted", "ChatMemberLeft", "ChatMemberBanned",
}
name := unionNameByVariants(variants)
require.Equal(t, "ChatMember", name)
}
func TestUnionNameByVariants_Unknown(t *testing.T) {
name := unionNameByVariants([]string{"X", "Y", "Z"})
require.Equal(t, "", name)
}
// ---------------------------------------------------------------------------
// hasUnionElem
// ---------------------------------------------------------------------------
func TestHasUnionElem_NonArray(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}
require.False(t, hasUnionElem(tr))
}
func TestHasUnionElem_ArrayNilElem(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindArray}
require.False(t, hasUnionElem(tr))
}
func TestHasUnionElem_ArrayUnionElem(t *testing.T) {
elem := spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}
tr := spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
require.True(t, hasUnionElem(tr))
}
func TestHasUnionElem_ArrayNonUnionElem(t *testing.T) {
elem := spec.TypeRef{Kind: spec.KindNamed, Name: "Message"}
tr := spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
require.False(t, hasUnionElem(tr))
}
// ---------------------------------------------------------------------------
// unionFieldsOf
// ---------------------------------------------------------------------------
func TestUnionFieldsOf_WithUnionField(t *testing.T) {
td := spec.TypeDecl{
Name: "ChatMemberUpdated",
Fields: []spec.Field{
{Name: "NewChatMember", JSONName: "new_chat_member", Type: spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}},
{Name: "OldChatMember", JSONName: "old_chat_member", Type: spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}},
{Name: "Date", JSONName: "date", Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "int64"}},
},
}
uf := unionFieldsOf(td)
require.Len(t, uf, 2)
require.Equal(t, "ChatMember", uf[0].UnionName)
}
// ---------------------------------------------------------------------------
// splitLines — edge cases
// ---------------------------------------------------------------------------
func TestSplitLines_Empty(t *testing.T) {
require.Empty(t, splitLines(""))
}
func TestSplitLines_NoNewline(t *testing.T) {
got := splitLines("hello world")
require.Equal(t, []string{"hello world"}, got)
}
func TestSplitLines_TrailingNewline(t *testing.T) {
got := splitLines("line1\nline2\n")
require.Equal(t, []string{"line1", "line2"}, got)
}
func TestSplitLines_MultiLine(t *testing.T) {
got := splitLines("a\nb\nc")
require.Equal(t, []string{"a", "b", "c"}, got)
}
// ---------------------------------------------------------------------------
// docComment
// ---------------------------------------------------------------------------
func TestDocComment_Empty(t *testing.T) {
require.Equal(t, "", docComment(""))
}
func TestDocComment_SingleLine(t *testing.T) {
got := docComment("Hello world.")
require.Equal(t, "// Hello world.\n", got)
}
func TestDocComment_MultiLine(t *testing.T) {
got := docComment("Line 1\nLine 2")
require.Contains(t, got, "// Line 1\n")
require.Contains(t, got, "// Line 2\n")
}
// ---------------------------------------------------------------------------
// title
// ---------------------------------------------------------------------------
func TestTitle_Empty(t *testing.T) {
require.Equal(t, "", title(""))
}
func TestTitle_Lowercase(t *testing.T) {
require.Equal(t, "SendMessage", title("sendMessage"))
}
func TestTitle_AlreadyUpper(t *testing.T) {
require.Equal(t, "GetMe", title("GetMe"))
}
// ---------------------------------------------------------------------------
// mentionsInputFileTr
// ---------------------------------------------------------------------------
func TestMentionsInputFileTr_Named(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindNamed, Name: "InputFile"}
require.True(t, mentionsInputFileTr(tr))
}
func TestMentionsInputFileTr_NotInputFile(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindNamed, Name: "Message"}
require.False(t, mentionsInputFileTr(tr))
}
func TestMentionsInputFileTr_Array(t *testing.T) {
elem := spec.TypeRef{Kind: spec.KindNamed, Name: "InputFile"}
tr := spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
require.True(t, mentionsInputFileTr(tr))
}
func TestMentionsInputFileTr_ArrayNilElem(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindArray}
require.False(t, mentionsInputFileTr(tr))
}
func TestMentionsInputFileTr_OneOf_WithInputFile(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"InputFile", "string"}}
require.True(t, mentionsInputFileTr(tr))
}
func TestMentionsInputFileTr_OneOf_WithoutInputFile(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"A", "B"}}
require.False(t, mentionsInputFileTr(tr))
}
// ---------------------------------------------------------------------------
// loadAPI — error paths
// ---------------------------------------------------------------------------
func TestLoadAPI_MissingFile(t *testing.T) {
_, err := loadAPI("/nonexistent/path/api.json")
require.Error(t, err)
}
// ---------------------------------------------------------------------------
// runtimeTypes filter in emitTypes
// ---------------------------------------------------------------------------
func TestRuntimeTypes_NeverEmitted(t *testing.T) {
for name := range runtimeTypes {
require.True(t, runtimeTypes[name], "runtimeType %q should be true", name)
}
require.True(t, runtimeTypes["InputFile"])
require.True(t, runtimeTypes["ChatID"])
require.True(t, runtimeTypes["MessageOrBool"])
require.True(t, runtimeTypes["ResponseParameters"])
}
// ---------------------------------------------------------------------------
// sentinelForField — all branches
// ---------------------------------------------------------------------------
func TestSentinelForField(t *testing.T) {
unionTypes := map[string]bool{"ChatMember": true}
cases := []struct {
name string
field spec.Field
contains string
}{
{
name: "int64 primitive",
field: makeField("Count", "count", "int64", spec.KindPrimitive, true),
contains: "42",
},
{
name: "string primitive",
field: makeField("Text", "text", "string", spec.KindPrimitive, true),
contains: "test_value",
},
{
name: "bool primitive",
field: makeField("Flag", "flag", "bool", spec.KindPrimitive, true),
contains: "true",
},
{
name: "float64 primitive",
field: makeField("Lat", "lat", "float64", spec.KindPrimitive, true),
contains: "1.0",
},
{
name: "named ChatID",
field: makeField("ChatID", "chat_id", "ChatID", spec.KindNamed, true),
contains: "ChatIDFromInt",
},
{
name: "named InputFile",
field: makeField("Photo", "photo", "InputFile", spec.KindNamed, true),
contains: "InputFile",
},
{
name: "named union (nil-able)",
field: makeField("Member", "member", "ChatMember", spec.KindNamed, true),
contains: "nil",
},
{
name: "named required struct",
field: makeField("Chat", "chat", "Chat", spec.KindNamed, true),
contains: "Chat{}",
},
{
name: "named optional struct",
field: makeField("Chat", "chat", "Chat", spec.KindNamed, false),
contains: "&Chat{}",
},
{
name: "array",
field: spec.Field{Name: "Items", JSONName: "items", Type: spec.TypeRef{Kind: spec.KindArray}},
contains: "nil",
},
{
name: "oneOf ChatID variants",
field: makeFieldVariants("ChatID", "chat_id", spec.KindOneOf, []string{"int64", "string"}, true),
contains: "ChatIDFromInt",
},
{
name: "oneOf InputFile variants",
field: makeFieldVariants("Photo", "photo", spec.KindOneOf, []string{"InputFile", "string"}, true),
contains: "InputFile",
},
{
name: "oneOf sealed",
field: makeFieldVariants("Markup", "markup", spec.KindOneOf, []string{"A", "B"}, true),
contains: "nil",
},
{
name: "unknown kind",
field: spec.Field{Name: "X", JSONName: "x", Type: spec.TypeRef{Kind: spec.Kind(99)}},
contains: "nil",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := sentinelForField(c.field, unionTypes, nil)
require.Contains(t, got, c.contains, "sentinelForField for %q", c.name)
})
}
}
// ---------------------------------------------------------------------------
// successBody — all branches
// ---------------------------------------------------------------------------
func TestSuccessBody(t *testing.T) {
cases := []struct {
name string
tr spec.TypeRef
want string
}{
{"bool", spec.TypeRef{Kind: spec.KindPrimitive, Name: "bool"}, "true"},
{"int64", spec.TypeRef{Kind: spec.KindPrimitive, Name: "int64"}, "0"},
{"float64", spec.TypeRef{Kind: spec.KindPrimitive, Name: "float64"}, "0"},
{"string", spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}, `""`},
{"MessageOrBool", spec.TypeRef{Kind: spec.KindNamed, Name: "MessageOrBool"}, "true"},
{"named", spec.TypeRef{Kind: spec.KindNamed, Name: "Message"}, "{}"},
{"array", spec.TypeRef{Kind: spec.KindArray}, "[]"},
{"oneOf", spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"A", "B"}}, "null"},
{"unknown", spec.TypeRef{Kind: spec.Kind(99)}, "null"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := successBody(c.tr)
require.Equal(t, c.want, got)
})
}
}
// ---------------------------------------------------------------------------
// unionTypeFor — KindOneOf branch (variant-set match)
// ---------------------------------------------------------------------------
func TestUnionTypeFor_OneOfVariants(t *testing.T) {
// These variant *type names* match the ChatMember discriminator.
tr := spec.TypeRef{
Kind: spec.KindOneOf,
Variants: []string{
"ChatMemberOwner", "ChatMemberAdministrator", "ChatMemberMember",
"ChatMemberRestricted", "ChatMemberLeft", "ChatMemberBanned",
},
}
name, ok := unionTypeFor(tr)
require.True(t, ok)
require.Equal(t, "ChatMember", name)
}
func TestUnionTypeFor_OneOfNoMatch(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"Foo", "Bar"}}
_, ok := unionTypeFor(tr)
require.False(t, ok)
}
// ---------------------------------------------------------------------------
// funcs() returns a non-nil FuncMap with expected keys
// ---------------------------------------------------------------------------
func TestFuncs_HasExpectedKeys(t *testing.T) {
fm := funcs(nil)
require.NotNil(t, fm)
for _, key := range []string{"goType", "docComment", "returnGoType", "unionFields"} {
require.NotNil(t, fm[key], "funcs() missing key %q", key)
}
}