Files
go-telegram/api/unifiedenum_test.go
T
lukaszraczylo 5523ed2b06 refactor(api): unify per-variant single-value enums into shared union-level enums
Each sealed-interface union with N variants used to emit N typed-string
enums, one per variant, each holding exactly one wire value. Codegen now
detects this pattern and emits ONE unified enum at the union level,
retyping every variant's discriminator field to point at it.

Unified enums (11 unions, 44 constants total):
  - ChatMemberStatus           (6)
  - MessageOriginType          (4)
  - BackgroundFillType         (3)
  - BackgroundTypeKind         (4)
  - ChatBoostSourceKind        (3)
  - OwnedGiftType              (2)
  - PaidMediaType              (4)
  - ReactionTypeKind           (3)
  - RevenueWithdrawalStateKind (3)
  - StoryAreaTypeKind          (5)
  - TransactionPartnerType     (7)

Naming-collision cases (union name already ends in a discriminator
concept noun, so the natural concat would stutter): BackgroundType,
ReactionType, StoryAreaType, ChatBoostSource, RevenueWithdrawalState.
The unified name ends with 'Kind' instead.

44 obsolete per-variant single-value enum types removed (e.g.
ChatMemberOwnerStatus, MessageOriginUserType). The variant struct types
themselves (ChatMemberOwner etc.) are unchanged; only their per-variant
single-value enum aliases go away.

Auto-inject MarshalJSON (commit 370c9c0) is unaffected — variant wire
values still come from the discriminator-extractor pass.

Call-site cleanup:
  - dispatch/filters/chatmember/chatmember_test.go
  - api/marshaljson_variants_test.go

New test: api/unifiedenum_test.go covers ChatMemberStatus and
MessageOriginType: variant-field retype, direct comparison without
conversion, marshal discriminator preservation, full round-trip,
stutter-suffix Kind sanity check.
2026-05-09 20:26:08 +01:00

183 lines
6.8 KiB
Go

package api
import (
"reflect"
"testing"
json "github.com/goccy/go-json"
"github.com/stretchr/testify/require"
)
// TestUnifiedEnum_ChatMemberStatus_HasAllConstants asserts the unified
// enum exists with the full set of variant values and is a typed string.
func TestUnifiedEnum_ChatMemberStatus_HasAllConstants(t *testing.T) {
require.IsType(t, ChatMemberStatus(""), ChatMemberStatusCreator)
values := []ChatMemberStatus{
ChatMemberStatusCreator,
ChatMemberStatusAdministrator,
ChatMemberStatusMember,
ChatMemberStatusRestricted,
ChatMemberStatusLeft,
ChatMemberStatusKicked,
}
wantWire := []string{"creator", "administrator", "member", "restricted", "left", "kicked"}
require.Len(t, values, 6)
for i, v := range values {
require.Equal(t, wantWire[i], string(v))
}
}
// TestUnifiedEnum_ChatMember_VariantFieldsRetyped confirms every concrete
// variant's discriminator field is the unified enum, NOT a per-variant
// alias type. Reflection walks the struct field directly.
func TestUnifiedEnum_ChatMember_VariantFieldsRetyped(t *testing.T) {
cases := []struct {
name string
val any
}{
{"ChatMemberOwner", &ChatMemberOwner{}},
{"ChatMemberAdministrator", &ChatMemberAdministrator{}},
{"ChatMemberMember", &ChatMemberMember{}},
{"ChatMemberRestricted", &ChatMemberRestricted{}},
{"ChatMemberLeft", &ChatMemberLeft{}},
{"ChatMemberBanned", &ChatMemberBanned{}},
}
wantType := reflect.TypeOf(ChatMemberStatus(""))
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
f, ok := reflect.TypeOf(tc.val).Elem().FieldByName("Status")
require.True(t, ok, "%s missing Status field", tc.name)
require.Equal(t, wantType, f.Type, "%s.Status type mismatch", tc.name)
})
}
}
// TestUnifiedEnum_ChatMember_DirectComparison verifies the unified enum
// lets callers compare a variant's Status directly against constants
// without conversion.
func TestUnifiedEnum_ChatMember_DirectComparison(t *testing.T) {
owner := &ChatMemberOwner{Status: ChatMemberStatusCreator}
require.True(t, owner.Status == ChatMemberStatusCreator)
require.False(t, owner.Status == ChatMemberStatusKicked)
banned := &ChatMemberBanned{Status: ChatMemberStatusKicked}
require.True(t, banned.Status == ChatMemberStatusKicked)
}
// TestUnifiedEnum_ChatMember_MarshalDiscriminator verifies the auto-inject
// MarshalJSON still emits the right wire discriminator after the enum
// retype — no regression from commit 370c9c0.
func TestUnifiedEnum_ChatMember_MarshalDiscriminator(t *testing.T) {
cases := []struct {
val any
wantWire string
}{
{&ChatMemberOwner{User: User{ID: 1, FirstName: "a"}}, "creator"},
{&ChatMemberAdministrator{User: User{ID: 2, FirstName: "b"}}, "administrator"},
{&ChatMemberMember{User: User{ID: 3, FirstName: "c"}}, "member"},
{&ChatMemberRestricted{User: User{ID: 4, FirstName: "d"}}, "restricted"},
{&ChatMemberLeft{User: User{ID: 5, FirstName: "e"}}, "left"},
{&ChatMemberBanned{User: User{ID: 6, FirstName: "f"}}, "kicked"},
}
for _, tc := range cases {
raw, err := json.Marshal(tc.val)
require.NoError(t, err)
var probe struct {
Status string `json:"status"`
}
require.NoError(t, json.Unmarshal(raw, &probe))
require.Equal(t, tc.wantWire, probe.Status)
}
}
// TestUnifiedEnum_ChatMember_RoundTrip confirms a marshal-unmarshal cycle
// preserves the unified-enum field value.
func TestUnifiedEnum_ChatMember_RoundTrip(t *testing.T) {
orig := &ChatMemberOwner{User: User{ID: 99, FirstName: "owner"}}
raw, err := json.Marshal(orig)
require.NoError(t, err)
out, err := UnmarshalChatMember(raw)
require.NoError(t, err)
round, ok := out.(*ChatMemberOwner)
require.True(t, ok, "expected *ChatMemberOwner, got %T", out)
require.Equal(t, ChatMemberStatusCreator, round.Status)
require.Equal(t, orig.User.ID, round.User.ID)
}
// TestUnifiedEnum_MessageOriginType verifies a second union also unifies
// correctly — guards against a one-off implementation that only handles
// ChatMember.
func TestUnifiedEnum_MessageOriginType(t *testing.T) {
require.IsType(t, MessageOriginType(""), MessageOriginTypeUser)
values := []MessageOriginType{
MessageOriginTypeUser,
MessageOriginTypeHiddenUser,
MessageOriginTypeChat,
MessageOriginTypeChannel,
}
wantWire := []string{"user", "hidden_user", "chat", "channel"}
for i, v := range values {
require.Equal(t, wantWire[i], string(v))
}
// Variant fields use the unified type.
wantType := reflect.TypeOf(MessageOriginType(""))
for _, name := range []string{"MessageOriginUser", "MessageOriginHiddenUser", "MessageOriginChat", "MessageOriginChannel"} {
switch name {
case "MessageOriginUser":
f, ok := reflect.TypeOf(&MessageOriginUser{}).Elem().FieldByName("Type")
require.True(t, ok)
require.Equal(t, wantType, f.Type)
case "MessageOriginHiddenUser":
f, ok := reflect.TypeOf(&MessageOriginHiddenUser{}).Elem().FieldByName("Type")
require.True(t, ok)
require.Equal(t, wantType, f.Type)
case "MessageOriginChat":
f, ok := reflect.TypeOf(&MessageOriginChat{}).Elem().FieldByName("Type")
require.True(t, ok)
require.Equal(t, wantType, f.Type)
case "MessageOriginChannel":
f, ok := reflect.TypeOf(&MessageOriginChannel{}).Elem().FieldByName("Type")
require.True(t, ok)
require.Equal(t, wantType, f.Type)
}
}
}
// TestUnifiedEnum_StutterSuffix_Kind covers the naming-collision rule:
// when the union name ends in a discriminator concept noun, the unified
// enum is suffixed with "Kind" to avoid stuttery names like
// "BackgroundTypeType".
func TestUnifiedEnum_StutterSuffix_Kind(t *testing.T) {
require.IsType(t, BackgroundTypeKind(""), BackgroundTypeKindFill)
require.IsType(t, ReactionTypeKind(""), ReactionTypeKindEmoji)
require.IsType(t, StoryAreaTypeKind(""), StoryAreaTypeKindLocation)
require.IsType(t, ChatBoostSourceKind(""), ChatBoostSourceKindPremium)
require.IsType(t, RevenueWithdrawalStateKind(""), RevenueWithdrawalStateKindPending)
// Variant struct field types match the unified enum.
wantType := reflect.TypeOf(BackgroundTypeKind(""))
f, ok := reflect.TypeOf(&BackgroundTypeFill{}).Elem().FieldByName("Type")
require.True(t, ok)
require.Equal(t, wantType, f.Type)
}
// TestUnifiedEnum_PerVariantTypesNotEmitted asserts the obsolete
// per-variant single-value enum types (e.g. ChatMemberOwnerStatus) are
// gone — ensures the codegen doesn't double-emit. We rely on compile-time
// behaviour: if any of these names existed, a referencing package would
// fail to build. Instead we verify the variant struct field type's name
// is the unified one.
func TestUnifiedEnum_PerVariantTypesNotEmitted(t *testing.T) {
got := reflect.TypeOf(&ChatMemberOwner{}).Elem()
statusField, ok := got.FieldByName("Status")
require.True(t, ok)
require.Equal(t, "ChatMemberStatus", statusField.Type.Name(),
"ChatMemberOwner.Status should be ChatMemberStatus, not ChatMemberOwnerStatus")
}