From d666cda3b1de3787a48419d262fff988f81c3992 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Tue, 15 Sep 2026 09:53:17 +0100 Subject: [PATCH] feat(genapi): derive discriminators and decode RichText alternates The generator gated all union decoder emission behind knownDiscriminators, a hand-curated map of 13 entries. Any sealed union absent from that map received no decoder, so decoding must fail. Bot API v10 added RichBlock and RichText, which were absent. Roughly 8.6 percent of Telegram updates failed with HTTP 400. The generator now derives discriminators from the specification instead of a fixed list. A gate check fails the build when a union resolves no discriminator. Eleven unions gain decoders that never had one: BotCommandScope, InputMedia, InputPaidMedia, InputPollMedia, InputPollOptionMedia, InputProfilePhoto, InputRichBlock, InputStoryContent, PassportElementError, RichBlock and RichText. RichText can also appear as a bare string or an array of RichText. The scraper now extracts these alternate shapes into the intermediate representation. The emitter adds RichTextPlain and RichTextSequence. The decoder dispatches on the first JSON byte before it reads any discriminator. Unions without alternates must regenerate byte for byte. The golden test proves this. Claude-Session: https://claude.ai/code/session_01Fu2wXqGUrfVBi5aLafKk8U --- api/richmessage_test.go | 210 +++ api/types.gen.go | 2011 +++++++++++++++++++++- cmd/genapi/alternates_test.go | 144 ++ cmd/genapi/derive_discriminators_test.go | 148 ++ cmd/genapi/emitter.go | 397 ++++- cmd/genapi/enums.go | 28 +- cmd/genapi/extra_test.go | 30 +- cmd/genapi/main.go | 3 + cmd/genapi/types.tmpl | 71 +- cmd/scrape/alternates.go | 50 + cmd/scrape/alternates_test.go | 99 ++ cmd/scrape/scrape.go | 4 + docs/reference/api.md | 1908 +++++++++++++------- internal/spec/api.json | 8 + internal/spec/ir.go | 25 + testdata/golden/types.gen.go | 8 +- 16 files changed, 4449 insertions(+), 695 deletions(-) create mode 100644 api/richmessage_test.go create mode 100644 cmd/genapi/alternates_test.go create mode 100644 cmd/genapi/derive_discriminators_test.go create mode 100644 cmd/scrape/alternates.go create mode 100644 cmd/scrape/alternates_test.go diff --git a/api/richmessage_test.go b/api/richmessage_test.go new file mode 100644 index 0000000..3041236 --- /dev/null +++ b/api/richmessage_test.go @@ -0,0 +1,210 @@ +package api + +import ( + "testing" + + json "github.com/goccy/go-json" + "github.com/stretchr/testify/require" +) + +// TestUpdate_RichMessage_Decodes is a regression test for the Bot API v10 +// rich_message decode failure: RichBlock (21 variants) and RichText (25 +// variants) had no knownDiscriminators entry, so genapi never emitted +// UnmarshalRichBlock / UnmarshalRichText and any update whose message +// carried rich_message failed to decode — in production this rejected +// roughly 8.6% of Telegram updates with HTTP 400. Uses goccy/go-json, the +// codec production runs; plain encoding/json failed the same way. +func TestUpdate_RichMessage_Decodes(t *testing.T) { + body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` + + `"rich_message":{"blocks":[{"type":"paragraph","text":{"type":"bold","text":{"type":"anchor","name":"hi"}}}]}}}` + + var u Update + require.NoError(t, json.Unmarshal([]byte(body), &u)) + + require.NotNil(t, u.Message) + require.NotNil(t, u.Message.RichMessage) + require.Len(t, u.Message.RichMessage.Blocks, 1) + + para, ok := u.Message.RichMessage.Blocks[0].(*RichBlockParagraph) + require.True(t, ok, "blocks[0] must dispatch to *RichBlockParagraph, got %T", u.Message.RichMessage.Blocks[0]) + + bold, ok := para.Text.(*RichTextBold) + require.True(t, ok, "paragraph.text must dispatch to *RichTextBold, got %T", para.Text) + + anchor, ok := bold.Text.(*RichTextAnchor) + require.True(t, ok, "bold.text must dispatch to *RichTextAnchor, got %T", bold.Text) + require.Equal(t, "hi", anchor.Name) +} + +// TestUnmarshalRichBlock_UnknownType confirms an unrecognised discriminator +// value surfaces a clear error instead of silently decoding a zero-valued +// interface. +func TestUnmarshalRichBlock_UnknownType(t *testing.T) { + _, err := UnmarshalRichBlock([]byte(`{"type":"not_a_real_block"}`)) + require.Error(t, err) + require.ErrorContains(t, err, "unknown type") +} + +// TestUpdate_RichMessage_UnknownBlockType confirms the unknown-type error +// from UnmarshalRichBlock propagates all the way up through +// RichMessage.UnmarshalJSON to the top-level Update decode. +func TestUpdate_RichMessage_UnknownBlockType(t *testing.T) { + body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` + + `"rich_message":{"blocks":[{"type":"not_a_real_block"}]}}}` + + var u Update + err := json.Unmarshal([]byte(body), &u) + require.Error(t, err) + require.ErrorContains(t, err, "unknown type") +} + +// TestUpdate_RichMessage_PlainStringText is a regression test for the Bot +// API v10 RichText decode gap this task fixes. Telegram declares RichText +// as decodable from "either a String for plain text, an Array of +// RichText, or any of the following [object] types" — but the IR (and +// therefore UnmarshalRichText) only ever recorded the object variants, so +// a paragraph whose text is a bare JSON string failed with "invalid +// character '\"' looking for beginning of value". A plain-text paragraph +// is the most common real rich_message payload, so this was the +// production-facing half of the bug. +func TestUpdate_RichMessage_PlainStringText(t *testing.T) { + body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` + + `"rich_message":{"blocks":[{"type":"paragraph","text":"hello world"}]}}}` + + var u Update + require.NoError(t, json.Unmarshal([]byte(body), &u)) + + require.NotNil(t, u.Message) + require.NotNil(t, u.Message.RichMessage) + require.Len(t, u.Message.RichMessage.Blocks, 1) + + para, ok := u.Message.RichMessage.Blocks[0].(*RichBlockParagraph) + require.True(t, ok, "blocks[0] must dispatch to *RichBlockParagraph, got %T", u.Message.RichMessage.Blocks[0]) + + plain, ok := para.Text.(*RichTextPlain) + require.True(t, ok, "paragraph.text must dispatch to *RichTextPlain for a bare JSON string, got %T", para.Text) + require.Equal(t, "hello world", plain.Text) +} + +// TestUpdate_RichMessage_ArrayText confirms Telegram's second declared +// RichText alternate — a bare JSON array of RichText — decodes into +// RichTextSequence, with each element dispatched recursively through +// UnmarshalRichText (an object variant in this case). +func TestUpdate_RichMessage_ArrayText(t *testing.T) { + body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` + + `"rich_message":{"blocks":[{"type":"paragraph","text":[` + + `{"type":"bold","text":"x"},{"type":"italic","text":"y"}` + + `]}]}}}` + + var u Update + require.NoError(t, json.Unmarshal([]byte(body), &u)) + + para, ok := u.Message.RichMessage.Blocks[0].(*RichBlockParagraph) + require.True(t, ok) + + seq, ok := para.Text.(*RichTextSequence) + require.True(t, ok, "paragraph.text must dispatch to *RichTextSequence for a JSON array, got %T", para.Text) + require.Len(t, seq.Items, 2) + + bold, ok := seq.Items[0].(*RichTextBold) + require.True(t, ok, "sequence[0] must be *RichTextBold, got %T", seq.Items[0]) + boldText, ok := bold.Text.(*RichTextPlain) + require.True(t, ok) + require.Equal(t, "x", boldText.Text) + + italic, ok := seq.Items[1].(*RichTextItalic) + require.True(t, ok, "sequence[1] must be *RichTextItalic, got %T", seq.Items[1]) + italicText, ok := italic.Text.(*RichTextPlain) + require.True(t, ok) + require.Equal(t, "y", italicText.Text) +} + +// TestUpdate_RichMessage_ArrayText_MixedElements confirms an array whose +// elements mix the bare-string shape and the object shape dispatches +// each element independently — the array alternate and the string +// alternate compose, they aren't mutually exclusive per element. +func TestUpdate_RichMessage_ArrayText_MixedElements(t *testing.T) { + body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` + + `"rich_message":{"blocks":[{"type":"paragraph","text":[` + + `"plain segment",{"type":"bold","text":"bold segment"}` + + `]}]}}}` + + var u Update + require.NoError(t, json.Unmarshal([]byte(body), &u)) + + para, ok := u.Message.RichMessage.Blocks[0].(*RichBlockParagraph) + require.True(t, ok) + + seq, ok := para.Text.(*RichTextSequence) + require.True(t, ok) + require.Len(t, seq.Items, 2) + + plain, ok := seq.Items[0].(*RichTextPlain) + require.True(t, ok, "sequence[0] must be *RichTextPlain, got %T", seq.Items[0]) + require.Equal(t, "plain segment", plain.Text) + + bold, ok := seq.Items[1].(*RichTextBold) + require.True(t, ok, "sequence[1] must be *RichTextBold, got %T", seq.Items[1]) + boldText, ok := bold.Text.(*RichTextPlain) + require.True(t, ok) + require.Equal(t, "bold segment", boldText.Text) +} + +// TestRichTextPlain_MarshalJSON_RoundTrip confirms the plain-text +// alternate marshals back to a bare JSON string (not an object), and +// that string decodes straight back through UnmarshalRichText. +func TestRichTextPlain_MarshalJSON_RoundTrip(t *testing.T) { + v := &RichTextPlain{Text: "hello"} + + b, err := json.Marshal(v) + require.NoError(t, err) + require.JSONEq(t, `"hello"`, string(b)) + + decoded, err := UnmarshalRichText(b) + require.NoError(t, err) + got, ok := decoded.(*RichTextPlain) + require.True(t, ok) + require.Equal(t, "hello", got.Text) +} + +// TestRichTextSequence_MarshalJSON_RoundTrip confirms the array +// alternate marshals back to a bare JSON array (not an object wrapping +// one), preserving each element's own wire shape, and round-trips +// through UnmarshalRichText. +func TestRichTextSequence_MarshalJSON_RoundTrip(t *testing.T) { + v := &RichTextSequence{ + Items: []RichText{ + &RichTextPlain{Text: "a"}, + &RichTextBold{Text: &RichTextPlain{Text: "b"}}, + }, + } + + b, err := json.Marshal(v) + require.NoError(t, err) + require.JSONEq(t, `["a",{"type":"bold","text":"b"}]`, string(b)) + + decoded, err := UnmarshalRichText(b) + require.NoError(t, err) + seq, ok := decoded.(*RichTextSequence) + require.True(t, ok) + require.Len(t, seq.Items, 2) + + p0, ok := seq.Items[0].(*RichTextPlain) + require.True(t, ok) + require.Equal(t, "a", p0.Text) + + b1, ok := seq.Items[1].(*RichTextBold) + require.True(t, ok) + p1, ok := b1.Text.(*RichTextPlain) + require.True(t, ok) + require.Equal(t, "b", p1.Text) +} + +// TestUnmarshalRichText_UnrecognisedShape confirms a JSON value that is +// neither a string, an array, nor an object surfaces a clear error +// instead of silently producing a zero-valued interface. +func TestUnmarshalRichText_UnrecognisedShape(t *testing.T) { + _, err := UnmarshalRichText([]byte(`42`)) + require.Error(t, err) + require.ErrorContains(t, err, "unrecognised JSON value") +} diff --git a/api/types.gen.go b/api/types.gen.go index cd34008..570ff05 100644 --- a/api/types.gen.go +++ b/api/types.gen.go @@ -7,14 +7,16 @@ package api import ( + "bytes" "fmt" "github.com/goccy/go-json" "io" ) -var _ = io.Discard // keep import even if no fields use io -var _ = json.Marshal // keep import for UnmarshalXxx helpers -var _ = fmt.Errorf // keep import for UnmarshalXxx helpers +var _ = io.Discard // keep import even if no fields use io +var _ = json.Marshal // keep import for UnmarshalXxx helpers +var _ = fmt.Errorf // keep import for UnmarshalXxx helpers +var _ = bytes.TrimLeft // keep import for shape-dispatching UnmarshalXxx helpers (unions with alternates) // This object represents an incoming update.At most one of the optional fields can be present in any given update. type Update struct { @@ -1394,6 +1396,42 @@ func (*InputMediaVenue) isInputPollMedia() {} // isInputPollMedia is the marker method that makes InputMediaVideo implement InputPollMedia. func (*InputMediaVideo) isInputPollMedia() {} +// UnmarshalInputPollMedia decodes a InputPollMedia from JSON by inspecting the +// "type" field and dispatching to the correct concrete type. +func UnmarshalInputPollMedia(data []byte) (InputPollMedia, error) { + var probe struct { + V string `json:"type"` + } + if err := json.Unmarshal(data, &probe); err != nil { + return nil, err + } + var v InputPollMedia + switch probe.V { + case "animation": + v = &InputMediaAnimation{} + case "audio": + v = &InputMediaAudio{} + case "document": + v = &InputMediaDocument{} + case "live_photo": + v = &InputMediaLivePhoto{} + case "location": + v = &InputMediaLocation{} + case "photo": + v = &InputMediaPhoto{} + case "venue": + v = &InputMediaVenue{} + case "video": + v = &InputMediaVideo{} + default: + return nil, fmt.Errorf("InputPollMedia: unknown type %q", probe.V) + } + if err := json.Unmarshal(data, v); err != nil { + return nil, err + } + return v, nil +} + // InputPollOptionMedia is a union type. The following concrete variants implement // it: // - InputMediaAnimation @@ -1432,6 +1470,42 @@ func (*InputMediaVenue) isInputPollOptionMedia() {} // isInputPollOptionMedia is the marker method that makes InputMediaVideo implement InputPollOptionMedia. func (*InputMediaVideo) isInputPollOptionMedia() {} +// UnmarshalInputPollOptionMedia decodes a InputPollOptionMedia from JSON by inspecting the +// "type" field and dispatching to the correct concrete type. +func UnmarshalInputPollOptionMedia(data []byte) (InputPollOptionMedia, error) { + var probe struct { + V string `json:"type"` + } + if err := json.Unmarshal(data, &probe); err != nil { + return nil, err + } + var v InputPollOptionMedia + switch probe.V { + case "animation": + v = &InputMediaAnimation{} + case "link": + v = &InputMediaLink{} + case "live_photo": + v = &InputMediaLivePhoto{} + case "location": + v = &InputMediaLocation{} + case "photo": + v = &InputMediaPhoto{} + case "sticker": + v = &InputMediaSticker{} + case "venue": + v = &InputMediaVenue{} + case "video": + v = &InputMediaVideo{} + default: + return nil, fmt.Errorf("InputPollOptionMedia: unknown type %q", probe.V) + } + if err := json.Unmarshal(data, v); err != nil { + return nil, err + } + return v, nil +} + // This object contains information about one answer option in a poll. type PollOption struct { // Unique identifier of the option, persistent on option addition and deletion @@ -1464,6 +1538,30 @@ type InputPollOption struct { Media InputPollOptionMedia `json:"media,omitempty"` } +// UnmarshalJSON decodes InputPollOption by dispatching union-typed fields +// (Media) through their concrete UnmarshalXxx helpers. +func (m *InputPollOption) UnmarshalJSON(data []byte) error { + type Alias InputPollOption + aux := &struct { + Media json.RawMessage `json:"media,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Media) > 0 && string(aux.Media) != "null" { + v, err := UnmarshalInputPollOptionMedia(aux.Media) + if err != nil { + return fmt.Errorf("decoding media: %w", err) + } + m.Media = v + + } + + return nil +} + // This object represents an answer of a user in a non-anonymous poll. type PollAnswer struct { // Unique poll identifier @@ -4191,6 +4289,40 @@ func (*BotCommandScopeChatAdministrators) isBotCommandScope() {} // isBotCommandScope is the marker method that makes BotCommandScopeChatMember implement BotCommandScope. func (*BotCommandScopeChatMember) isBotCommandScope() {} +// UnmarshalBotCommandScope decodes a BotCommandScope from JSON by inspecting the +// "type" field and dispatching to the correct concrete type. +func UnmarshalBotCommandScope(data []byte) (BotCommandScope, error) { + var probe struct { + V string `json:"type"` + } + if err := json.Unmarshal(data, &probe); err != nil { + return nil, err + } + var v BotCommandScope + switch probe.V { + case "all_chat_administrators": + v = &BotCommandScopeAllChatAdministrators{} + case "all_group_chats": + v = &BotCommandScopeAllGroupChats{} + case "all_private_chats": + v = &BotCommandScopeAllPrivateChats{} + case "chat": + v = &BotCommandScopeChat{} + case "chat_administrators": + v = &BotCommandScopeChatAdministrators{} + case "chat_member": + v = &BotCommandScopeChatMember{} + case "default": + v = &BotCommandScopeDefault{} + default: + return nil, fmt.Errorf("BotCommandScope: unknown type %q", probe.V) + } + if err := json.Unmarshal(data, v); err != nil { + return nil, err + } + return v, nil +} + // Represents the default scope of bot commands. Default commands are used if no commands with a narrower scope are specified for the user. type BotCommandScopeDefault struct { // Scope type, must be default @@ -4820,6 +4952,38 @@ func (*InputMediaPhoto) isInputMedia() {} // isInputMedia is the marker method that makes InputMediaVideo implement InputMedia. func (*InputMediaVideo) isInputMedia() {} +// UnmarshalInputMedia decodes a InputMedia from JSON by inspecting the +// "type" field and dispatching to the correct concrete type. +func UnmarshalInputMedia(data []byte) (InputMedia, error) { + var probe struct { + V string `json:"type"` + } + if err := json.Unmarshal(data, &probe); err != nil { + return nil, err + } + var v InputMedia + switch probe.V { + case "animation": + v = &InputMediaAnimation{} + case "audio": + v = &InputMediaAudio{} + case "document": + v = &InputMediaDocument{} + case "live_photo": + v = &InputMediaLivePhoto{} + case "photo": + v = &InputMediaPhoto{} + case "video": + v = &InputMediaVideo{} + default: + return nil, fmt.Errorf("InputMedia: unknown type %q", probe.V) + } + if err := json.Unmarshal(data, v); err != nil { + return nil, err + } + return v, nil +} + // Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent. type InputMediaAnimation struct { // Type of the media, must be animation @@ -5202,6 +5366,32 @@ func (*InputPaidMediaPhoto) isInputPaidMedia() {} // isInputPaidMedia is the marker method that makes InputPaidMediaVideo implement InputPaidMedia. func (*InputPaidMediaVideo) isInputPaidMedia() {} +// UnmarshalInputPaidMedia decodes a InputPaidMedia from JSON by inspecting the +// "type" field and dispatching to the correct concrete type. +func UnmarshalInputPaidMedia(data []byte) (InputPaidMedia, error) { + var probe struct { + V string `json:"type"` + } + if err := json.Unmarshal(data, &probe); err != nil { + return nil, err + } + var v InputPaidMedia + switch probe.V { + case "live_photo": + v = &InputPaidMediaLivePhoto{} + case "photo": + v = &InputPaidMediaPhoto{} + case "video": + v = &InputPaidMediaVideo{} + default: + return nil, fmt.Errorf("InputPaidMedia: unknown type %q", probe.V) + } + if err := json.Unmarshal(data, v); err != nil { + return nil, err + } + return v, nil +} + // The paid media to send is a live photo. type InputPaidMediaLivePhoto struct { // Type of the media, must be live_photo @@ -5304,6 +5494,30 @@ func (*InputProfilePhotoStatic) isInputProfilePhoto() {} // isInputProfilePhoto is the marker method that makes InputProfilePhotoAnimated implement InputProfilePhoto. func (*InputProfilePhotoAnimated) isInputProfilePhoto() {} +// UnmarshalInputProfilePhoto decodes a InputProfilePhoto from JSON by inspecting the +// "type" field and dispatching to the correct concrete type. +func UnmarshalInputProfilePhoto(data []byte) (InputProfilePhoto, error) { + var probe struct { + V string `json:"type"` + } + if err := json.Unmarshal(data, &probe); err != nil { + return nil, err + } + var v InputProfilePhoto + switch probe.V { + case "animated": + v = &InputProfilePhotoAnimated{} + case "static": + v = &InputProfilePhotoStatic{} + default: + return nil, fmt.Errorf("InputProfilePhoto: unknown type %q", probe.V) + } + if err := json.Unmarshal(data, v); err != nil { + return nil, err + } + return v, nil +} + // A static profile photo in the .JPG format. type InputProfilePhotoStatic struct { // Type of the profile photo, must be static @@ -5368,6 +5582,30 @@ func (*InputStoryContentPhoto) isInputStoryContent() {} // isInputStoryContent is the marker method that makes InputStoryContentVideo implement InputStoryContent. func (*InputStoryContentVideo) isInputStoryContent() {} +// UnmarshalInputStoryContent decodes a InputStoryContent from JSON by inspecting the +// "type" field and dispatching to the correct concrete type. +func UnmarshalInputStoryContent(data []byte) (InputStoryContent, error) { + var probe struct { + V string `json:"type"` + } + if err := json.Unmarshal(data, &probe); err != nil { + return nil, err + } + var v InputStoryContent + switch probe.V { + case "photo": + v = &InputStoryContentPhoto{} + case "video": + v = &InputStoryContentVideo{} + default: + return nil, fmt.Errorf("InputStoryContent: unknown type %q", probe.V) + } + if err := json.Unmarshal(data, v); err != nil { + return nil, err + } + return v, nil +} + // Describes a photo to post as a story. type InputStoryContentPhoto struct { // Type of the content, must be photo @@ -5504,6 +5742,38 @@ type RichMessage struct { IsRtl *bool `json:"is_rtl,omitempty"` } +// UnmarshalJSON decodes RichMessage by dispatching union-typed fields +// (Blocks) through their concrete UnmarshalXxx helpers. +func (m *RichMessage) UnmarshalJSON(data []byte) error { + type Alias RichMessage + aux := &struct { + Blocks json.RawMessage `json:"blocks,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Blocks) > 0 && string(aux.Blocks) != "null" { + var raws []json.RawMessage + if err := json.Unmarshal(aux.Blocks, &raws); err != nil { + return fmt.Errorf("decoding blocks: %w", err) + } + decoded := make([]RichBlock, 0, len(raws)) + for i, r := range raws { + v, err := UnmarshalRichBlock(r) + if err != nil { + return fmt.Errorf("decoding blocks[%d]: %w", i, err) + } + decoded = append(decoded, v) + } + m.Blocks = decoded + + } + + return nil +} + // Describes a rich message to be sent. Exactly one of the fields html, markdown, or blocks must be used. type InputRichMessage struct { // Optional. Content of the rich message to send described as a list of blocks @@ -5520,6 +5790,38 @@ type InputRichMessage struct { SkipEntityDetection *bool `json:"skip_entity_detection,omitempty"` } +// UnmarshalJSON decodes InputRichMessage by dispatching union-typed fields +// (Blocks) through their concrete UnmarshalXxx helpers. +func (m *InputRichMessage) UnmarshalJSON(data []byte) error { + type Alias InputRichMessage + aux := &struct { + Blocks json.RawMessage `json:"blocks,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Blocks) > 0 && string(aux.Blocks) != "null" { + var raws []json.RawMessage + if err := json.Unmarshal(aux.Blocks, &raws); err != nil { + return fmt.Errorf("decoding blocks: %w", err) + } + decoded := make([]InputRichBlock, 0, len(raws)) + for i, r := range raws { + v, err := UnmarshalInputRichBlock(r) + if err != nil { + return fmt.Errorf("decoding blocks[%d]: %w", i, err) + } + decoded = append(decoded, v) + } + m.Blocks = decoded + + } + + return nil +} + // Describes a media element embedded in an outgoing rich message. type InputRichMessageMedia struct { // Unique identifier of the media used in a tg://photo?id=, tg://video?id=, tg://document?id=, or tg://audio?id= link. 1-64 characters, only A-Z, a-z, 0-9, _ and - are allowed. @@ -5554,6 +5856,30 @@ type RichMessageButton struct { Disabled *DisabledButton `json:"disabled,omitempty"` } +// UnmarshalJSON decodes RichMessageButton by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichMessageButton) UnmarshalJSON(data []byte) error { + type Alias RichMessageButton + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // RichText is a union type. The following concrete variants implement // it: // - RichTextBold @@ -5582,6 +5908,8 @@ type RichMessageButton struct { // - RichTextAnchorLink // - RichTextReference // - RichTextReferenceLink +// - RichTextPlain (bare JSON string) +// - RichTextSequence (JSON array of RichText) // // This object represents a rich formatted text. Currently, it can be either a String for plain text, an Array of RichText, or any of the following types: type RichText interface{ isRichText() } @@ -5664,6 +5992,139 @@ func (*RichTextReference) isRichText() {} // isRichText is the marker method that makes RichTextReferenceLink implement RichText. func (*RichTextReferenceLink) isRichText() {} +// RichTextPlain implements RichText for the bare JSON string wire +// shape Telegram documents for this union, alongside its object variants. +type RichTextPlain struct { + Text string +} + +// isRichText is the marker method that makes RichTextPlain implement RichText. +func (*RichTextPlain) isRichText() {} + +// MarshalJSON encodes RichTextPlain back to its bare JSON string wire shape. +func (v *RichTextPlain) MarshalJSON() ([]byte, error) { + return json.Marshal(v.Text) +} + +// RichTextSequence implements RichText for the JSON array of RichText wire +// shape Telegram documents for this union, alongside its object variants. +type RichTextSequence struct { + Items []RichText +} + +// isRichText is the marker method that makes RichTextSequence implement RichText. +func (*RichTextSequence) isRichText() {} + +// MarshalJSON encodes RichTextSequence back to its bare JSON array wire shape. +func (v *RichTextSequence) MarshalJSON() ([]byte, error) { + return json.Marshal(v.Items) +} + +// UnmarshalRichText decodes a RichText from JSON. Telegram documents +// this type as either a bare JSON string, a JSON array of RichText, or +// an object dispatched by its "type" field; +// leading whitespace is trimmed and the first remaining byte selects +// the shape. +func UnmarshalRichText(data []byte) (RichText, error) { + trimmed := bytes.TrimLeft(data, " \t\r\n") + if len(trimmed) == 0 { + return nil, fmt.Errorf("RichText: empty JSON value") + } + switch trimmed[0] { + case '"': + var s string + if err := json.Unmarshal(data, &s); err != nil { + return nil, fmt.Errorf("RichText: decoding string: %w", err) + } + return &RichTextPlain{Text: s}, nil + case '[': + var raws []json.RawMessage + if err := json.Unmarshal(data, &raws); err != nil { + return nil, fmt.Errorf("RichText: decoding array: %w", err) + } + items := make([]RichText, 0, len(raws)) + for i, r := range raws { + v, err := UnmarshalRichText(r) + if err != nil { + return nil, fmt.Errorf("RichText: decoding array[%d]: %w", i, err) + } + items = append(items, v) + } + return &RichTextSequence{Items: items}, nil + case '{': + // fall through to the discriminator switch below. + default: + return nil, fmt.Errorf("RichText: unrecognised JSON value starting with %q", trimmed[0]) + } + var probe struct { + V string `json:"type"` + } + if err := json.Unmarshal(data, &probe); err != nil { + return nil, err + } + var v RichText + switch probe.V { + case "anchor": + v = &RichTextAnchor{} + case "anchor_link": + v = &RichTextAnchorLink{} + case "bank_card_number": + v = &RichTextBankCardNumber{} + case "bold": + v = &RichTextBold{} + case "bot_command": + v = &RichTextBotCommand{} + case "button": + v = &RichTextButton{} + case "cashtag": + v = &RichTextCashtag{} + case "code": + v = &RichTextCode{} + case "custom_emoji": + v = &RichTextCustomEmoji{} + case "date_time": + v = &RichTextDateTime{} + case "email_address": + v = &RichTextEmailAddress{} + case "hashtag": + v = &RichTextHashtag{} + case "italic": + v = &RichTextItalic{} + case "marked": + v = &RichTextMarked{} + case "mathematical_expression": + v = &RichTextMathematicalExpression{} + case "mention": + v = &RichTextMention{} + case "phone_number": + v = &RichTextPhoneNumber{} + case "reference": + v = &RichTextReference{} + case "reference_link": + v = &RichTextReferenceLink{} + case "spoiler": + v = &RichTextSpoiler{} + case "strikethrough": + v = &RichTextStrikethrough{} + case "subscript": + v = &RichTextSubscript{} + case "superscript": + v = &RichTextSuperscript{} + case "text_mention": + v = &RichTextTextMention{} + case "underline": + v = &RichTextUnderline{} + case "url": + v = &RichTextUrl{} + default: + return nil, fmt.Errorf("RichText: unknown type %q", probe.V) + } + if err := json.Unmarshal(data, v); err != nil { + return nil, err + } + return v, nil +} + // A bold text. type RichTextBold struct { // Type of the rich text, always “bold” @@ -5688,6 +6149,30 @@ func (v *RichTextBold) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextBold by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextBold) UnmarshalJSON(data []byte) error { + type Alias RichTextBold + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // An italicized text. type RichTextItalic struct { // Type of the rich text, always “italic” @@ -5712,6 +6197,30 @@ func (v *RichTextItalic) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextItalic by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextItalic) UnmarshalJSON(data []byte) error { + type Alias RichTextItalic + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // An underlined text. type RichTextUnderline struct { // Type of the rich text, always “underline” @@ -5736,6 +6245,30 @@ func (v *RichTextUnderline) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextUnderline by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextUnderline) UnmarshalJSON(data []byte) error { + type Alias RichTextUnderline + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A strikethrough text. type RichTextStrikethrough struct { // Type of the rich text, always “strikethrough” @@ -5760,6 +6293,30 @@ func (v *RichTextStrikethrough) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextStrikethrough by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextStrikethrough) UnmarshalJSON(data []byte) error { + type Alias RichTextStrikethrough + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A text covered by a spoiler. type RichTextSpoiler struct { // Type of the rich text, always “spoiler” @@ -5784,6 +6341,30 @@ func (v *RichTextSpoiler) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextSpoiler by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextSpoiler) UnmarshalJSON(data []byte) error { + type Alias RichTextSpoiler + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // Formatted date and time. type RichTextDateTime struct { // Type of the rich text, always “date_time” @@ -5812,6 +6393,30 @@ func (v *RichTextDateTime) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextDateTime by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextDateTime) UnmarshalJSON(data []byte) error { + type Alias RichTextDateTime + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A mention of a Telegram user by their identifier. type RichTextTextMention struct { // Type of the rich text, always “text_mention” @@ -5838,6 +6443,30 @@ func (v *RichTextTextMention) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextTextMention by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextTextMention) UnmarshalJSON(data []byte) error { + type Alias RichTextTextMention + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A subscript text. type RichTextSubscript struct { // Type of the rich text, always “subscript” @@ -5862,6 +6491,30 @@ func (v *RichTextSubscript) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextSubscript by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextSubscript) UnmarshalJSON(data []byte) error { + type Alias RichTextSubscript + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A superscript text. type RichTextSuperscript struct { // Type of the rich text, always “superscript” @@ -5886,6 +6539,30 @@ func (v *RichTextSuperscript) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextSuperscript by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextSuperscript) UnmarshalJSON(data []byte) error { + type Alias RichTextSuperscript + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A marked text. type RichTextMarked struct { // Type of the rich text, always “marked” @@ -5910,6 +6587,30 @@ func (v *RichTextMarked) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextMarked by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextMarked) UnmarshalJSON(data []byte) error { + type Alias RichTextMarked + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A monowidth text. type RichTextCode struct { // Type of the rich text, always “code” @@ -5934,6 +6635,30 @@ func (v *RichTextCode) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextCode by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextCode) UnmarshalJSON(data []byte) error { + type Alias RichTextCode + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A custom emoji. type RichTextCustomEmoji struct { // Type of the rich text, always “custom_emoji” @@ -6010,6 +6735,30 @@ func (v *RichTextUrl) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextUrl by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextUrl) UnmarshalJSON(data []byte) error { + type Alias RichTextUrl + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A text with an email address. type RichTextEmailAddress struct { // Type of the rich text, always “email_address” @@ -6036,6 +6785,30 @@ func (v *RichTextEmailAddress) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextEmailAddress by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextEmailAddress) UnmarshalJSON(data []byte) error { + type Alias RichTextEmailAddress + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A text with a phone number. type RichTextPhoneNumber struct { // Type of the rich text, always “phone_number” @@ -6062,6 +6835,30 @@ func (v *RichTextPhoneNumber) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextPhoneNumber by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextPhoneNumber) UnmarshalJSON(data []byte) error { + type Alias RichTextPhoneNumber + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A text with a bank card number. type RichTextBankCardNumber struct { // Type of the rich text, always “bank_card_number” @@ -6088,6 +6885,30 @@ func (v *RichTextBankCardNumber) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextBankCardNumber by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextBankCardNumber) UnmarshalJSON(data []byte) error { + type Alias RichTextBankCardNumber + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A mention by a username. type RichTextMention struct { // Type of the rich text, always “mention” @@ -6114,6 +6935,30 @@ func (v *RichTextMention) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextMention by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextMention) UnmarshalJSON(data []byte) error { + type Alias RichTextMention + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A hashtag. type RichTextHashtag struct { // Type of the rich text, always “hashtag” @@ -6140,6 +6985,30 @@ func (v *RichTextHashtag) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextHashtag by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextHashtag) UnmarshalJSON(data []byte) error { + type Alias RichTextHashtag + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A cashtag. type RichTextCashtag struct { // Type of the rich text, always “cashtag” @@ -6166,6 +7035,30 @@ func (v *RichTextCashtag) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextCashtag by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextCashtag) UnmarshalJSON(data []byte) error { + type Alias RichTextCashtag + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A bot command. type RichTextBotCommand struct { // Type of the rich text, always “bot_command” @@ -6192,6 +7085,30 @@ func (v *RichTextBotCommand) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextBotCommand by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextBotCommand) UnmarshalJSON(data []byte) error { + type Alias RichTextBotCommand + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A button. type RichTextButton struct { // Type of the rich text, always “button” @@ -6266,6 +7183,30 @@ func (v *RichTextAnchorLink) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextAnchorLink by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextAnchorLink) UnmarshalJSON(data []byte) error { + type Alias RichTextAnchorLink + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A reference. type RichTextReference struct { // Type of the rich text, always “reference” @@ -6292,6 +7233,30 @@ func (v *RichTextReference) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextReference by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextReference) UnmarshalJSON(data []byte) error { + type Alias RichTextReference + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A link to a reference. type RichTextReferenceLink struct { // Type of the rich text, always “reference_link” @@ -6318,6 +7283,30 @@ func (v *RichTextReferenceLink) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichTextReferenceLink by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichTextReferenceLink) UnmarshalJSON(data []byte) error { + type Alias RichTextReferenceLink + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // Caption of a rich formatted block. type RichBlockCaption struct { // Block caption @@ -6326,6 +7315,40 @@ type RichBlockCaption struct { Credit RichText `json:"credit,omitempty"` } +// UnmarshalJSON decodes RichBlockCaption by dispatching union-typed fields +// (Text, Credit) through their concrete UnmarshalXxx helpers. +func (m *RichBlockCaption) UnmarshalJSON(data []byte) error { + type Alias RichBlockCaption + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + Credit json.RawMessage `json:"credit,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + if len(aux.Credit) > 0 && string(aux.Credit) != "null" { + v, err := UnmarshalRichText(aux.Credit) + if err != nil { + return fmt.Errorf("decoding credit: %w", err) + } + m.Credit = v + + } + + return nil +} + // Cell in a table. type RichBlockTableCell struct { // Optional. Text in the cell. If omitted, then the cell is invisible. @@ -6342,6 +7365,30 @@ type RichBlockTableCell struct { Valign RichBlockTableCellValign `json:"valign"` } +// UnmarshalJSON decodes RichBlockTableCell by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichBlockTableCell) UnmarshalJSON(data []byte) error { + type Alias RichBlockTableCell + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // An item of a list. type RichBlockListItem struct { // Label of the item @@ -6358,6 +7405,38 @@ type RichBlockListItem struct { Type RichBlockListItemType `json:"type,omitempty"` } +// UnmarshalJSON decodes RichBlockListItem by dispatching union-typed fields +// (Blocks) through their concrete UnmarshalXxx helpers. +func (m *RichBlockListItem) UnmarshalJSON(data []byte) error { + type Alias RichBlockListItem + aux := &struct { + Blocks json.RawMessage `json:"blocks,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Blocks) > 0 && string(aux.Blocks) != "null" { + var raws []json.RawMessage + if err := json.Unmarshal(aux.Blocks, &raws); err != nil { + return fmt.Errorf("decoding blocks: %w", err) + } + decoded := make([]RichBlock, 0, len(raws)) + for i, r := range raws { + v, err := UnmarshalRichBlock(r) + if err != nil { + return fmt.Errorf("decoding blocks[%d]: %w", i, err) + } + decoded = append(decoded, v) + } + m.Blocks = decoded + + } + + return nil +} + // RichBlock is a union type. The following concrete variants implement // it: // - RichBlockParagraph @@ -6460,6 +7539,74 @@ func (*RichBlockVoiceNote) isRichBlock() {} // isRichBlock is the marker method that makes RichBlockThinking implement RichBlock. func (*RichBlockThinking) isRichBlock() {} +// UnmarshalRichBlock decodes a RichBlock from JSON by inspecting the +// "type" field and dispatching to the correct concrete type. +func UnmarshalRichBlock(data []byte) (RichBlock, error) { + var probe struct { + V string `json:"type"` + } + if err := json.Unmarshal(data, &probe); err != nil { + return nil, err + } + var v RichBlock + switch probe.V { + case "anchor": + v = &RichBlockAnchor{} + case "animation": + v = &RichBlockAnimation{} + case "audio": + v = &RichBlockAudio{} + case "blockquote": + v = &RichBlockBlockQuotation{} + case "buttons": + v = &RichBlockButtons{} + case "collage": + v = &RichBlockCollage{} + case "details": + v = &RichBlockDetails{} + case "divider": + v = &RichBlockDivider{} + case "document": + v = &RichBlockDocument{} + case "expandable_blockquote": + v = &RichBlockExpandableBlockQuotation{} + case "footer": + v = &RichBlockFooter{} + case "heading": + v = &RichBlockSectionHeading{} + case "list": + v = &RichBlockList{} + case "map": + v = &RichBlockMap{} + case "mathematical_expression": + v = &RichBlockMathematicalExpression{} + case "paragraph": + v = &RichBlockParagraph{} + case "photo": + v = &RichBlockPhoto{} + case "pre": + v = &RichBlockPreformatted{} + case "pullquote": + v = &RichBlockPullQuotation{} + case "slideshow": + v = &RichBlockSlideshow{} + case "table": + v = &RichBlockTable{} + case "thinking": + v = &RichBlockThinking{} + case "video": + v = &RichBlockVideo{} + case "voice_note": + v = &RichBlockVoiceNote{} + default: + return nil, fmt.Errorf("RichBlock: unknown type %q", probe.V) + } + if err := json.Unmarshal(data, v); err != nil { + return nil, err + } + return v, nil +} + // A text paragraph, corresponding to the HTML tag

. type RichBlockParagraph struct { // Type of the block, always “paragraph” @@ -6484,6 +7631,30 @@ func (v *RichBlockParagraph) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichBlockParagraph by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichBlockParagraph) UnmarshalJSON(data []byte) error { + type Alias RichBlockParagraph + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A section heading, corresponding to the HTML tags

,

,

,

,

, or
. type RichBlockSectionHeading struct { // Type of the block, always “heading” @@ -6510,6 +7681,30 @@ func (v *RichBlockSectionHeading) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes RichBlockSectionHeading by dispatching union-typed fields +// (Text) through their concrete UnmarshalXxx helpers. +func (m *RichBlockSectionHeading) UnmarshalJSON(data []byte) error { + type Alias RichBlockSectionHeading + aux := &struct { + Text json.RawMessage `json:"text,omitempty"` + *Alias + }{Alias: (*Alias)(m)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if len(aux.Text) > 0 && string(aux.Text) != "null" { + v, err := UnmarshalRichText(aux.Text) + if err != nil { + return fmt.Errorf("decoding text: %w", err) + } + m.Text = v + + } + + return nil +} + // A preformatted text block, corresponding to the nested HTML tags
 and .
 type RichBlockPreformatted struct {
 	// Type of the block, always “pre”
@@ -6536,6 +7731,30 @@ func (v *RichBlockPreformatted) MarshalJSON() ([]byte, error) {
 	})
 }
 
+// UnmarshalJSON decodes RichBlockPreformatted by dispatching union-typed fields
+// (Text) through their concrete UnmarshalXxx helpers.
+func (m *RichBlockPreformatted) UnmarshalJSON(data []byte) error {
+	type Alias RichBlockPreformatted
+	aux := &struct {
+		Text json.RawMessage `json:"text,omitempty"`
+		*Alias
+	}{Alias: (*Alias)(m)}
+	if err := json.Unmarshal(data, aux); err != nil {
+		return err
+	}
+
+	if len(aux.Text) > 0 && string(aux.Text) != "null" {
+		v, err := UnmarshalRichText(aux.Text)
+		if err != nil {
+			return fmt.Errorf("decoding text: %w", err)
+		}
+		m.Text = v
+
+	}
+
+	return nil
+}
+
 // A footer, corresponding to the HTML tag