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

111 lines
3.4 KiB
Go

// Package spec defines the intermediate representation produced by the
// Telegram Bot API scraper (cmd/scrape) and consumed by the code generator
// (cmd/genapi). It is committed as internal/spec/api.json so PR diffs read
// as a Telegram changelog.
package spec
import "fmt"
// API is the top-level IR document.
type API struct {
// Version is the Telegram Bot API version parsed from the "Recent changes" section of the docs page.
Version string `json:"version"`
// Types lists all object types in declaration order.
Types []TypeDecl `json:"types"`
// Methods lists all API methods in declaration order.
Methods []MethodDecl `json:"methods"`
}
// TypeDecl describes a Telegram object type.
type TypeDecl struct {
Name string `json:"name"`
Doc string `json:"doc,omitempty"`
Fields []Field `json:"fields,omitempty"`
// OneOf, when non-empty, indicates this type is a union and lists the concrete variant type names.
// Variants are emitted as concrete structs implementing a sealed interface.
OneOf []string `json:"one_of,omitempty"`
}
// MethodDecl describes a Telegram API method.
type MethodDecl struct {
Name string `json:"name"`
Doc string `json:"doc,omitempty"`
Params []Field `json:"params,omitempty"`
Returns TypeRef `json:"returns"`
// HasFiles is true when any parameter accepts an InputFile, requiring a multipart/form-data request.
HasFiles bool `json:"has_files,omitempty"`
}
// Field describes a single field on a type or a single parameter on a method.
type Field struct {
// Name is the Go-style identifier (e.g. "ChatID").
Name string `json:"name"`
// JSONName is the wire name (e.g. "chat_id").
JSONName string `json:"json_name"`
Type TypeRef `json:"type"`
Required bool `json:"required,omitempty"`
Doc string `json:"doc,omitempty"`
// EnumValues, when non-empty, lists the wire-level string values the
// scraper detected for an enum-like description ("can be A, B or C",
// "always X", parse_mode special-case). Order is doc order, deduped.
// Emitted as a typed Go enum that replaces the field's string type.
EnumValues []string `json:"enum_values,omitempty"`
}
// Kind enumerates TypeRef shapes.
type Kind int
const (
// KindPrimitive: int64, string, bool, float64.
KindPrimitive Kind = iota
// KindNamed: a TypeDecl by name.
KindNamed
// KindArray: ElemType is the element type.
KindArray
// KindOneOf: Variants lists discriminant union members.
KindOneOf
)
// String returns a stable, lowercase representation suitable for serialisation.
func (k Kind) String() string {
switch k {
case KindPrimitive:
return "primitive"
case KindNamed:
return "named"
case KindArray:
return "array"
case KindOneOf:
return "oneOf"
default:
return "unknown"
}
}
// MarshalText / UnmarshalText keep JSON output human-readable.
func (k Kind) MarshalText() ([]byte, error) { return []byte(k.String()), nil }
func (k *Kind) UnmarshalText(b []byte) error {
switch string(b) {
case "primitive":
*k = KindPrimitive
case "named":
*k = KindNamed
case "array":
*k = KindArray
case "oneOf":
*k = KindOneOf
default:
return fmt.Errorf("unknown Kind: %q", string(b))
}
return nil
}
// TypeRef is a structural reference used wherever a Field type is expressed.
type TypeRef struct {
Kind Kind `json:"kind"`
Name string `json:"name,omitempty"`
ElemType *TypeRef `json:"elem_type,omitempty"`
Variants []string `json:"variants,omitempty"`
}