From 7e2f1416e98e0e0ee7b3cb88f538d43bde21012b Mon Sep 17 00:00:00 2001 From: Paul Larsen Date: Sun, 1 Mar 2026 23:17:26 +0100 Subject: [PATCH 1/4] update lib to use new Attach interface to read from files --- file.go | 6 ++- request.go | 28 +++----------- scripts/generate/gen.go | 21 ++++++++++- scripts/generate/methods.go | 4 ++ scripts/generate/types.go | 75 +++++++++++++++++++++++++++++-------- 5 files changed, 94 insertions(+), 40 deletions(-) diff --git a/file.go b/file.go index 375c7fb..e797fc5 100644 --- a/file.go +++ b/file.go @@ -21,10 +21,14 @@ type InputFile interface { // This object represents the contents of a file to be uploaded, or a publicly accessible URL to be reused. // Files must be posted using multipart/form-data in the usual way that files are uploaded via the browser. type InputFileOrString interface { - Attach(name string, w *multipart.Writer) error + Attach getValue() string } +type Attach interface { + Attach(name string, w *multipart.Writer) error +} + var ( _ InputFileOrString = &FileReader{} _ InputFile = &FileReader{} diff --git a/request.go b/request.go index 7f0d508..c09ed2c 100644 --- a/request.go +++ b/request.go @@ -11,7 +11,6 @@ import ( "mime/multipart" "net/http" "net/url" - "strconv" "strings" "time" ) @@ -241,39 +240,24 @@ func getFieldContents(v any, k string, w *multipart.Writer) (string, error) { case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool: return fmt.Sprint(val), nil - case InputMedia: - err := val.InputParams(k, w) + case Attach: + err := val.Attach(k, w) if err != nil { return "", fmt.Errorf("failed to read input multipart field: %w", err) } - bs, err := json.Marshal(val) - if err != nil { - return "", fmt.Errorf("failed to marshal field %s to JSON: %w", k, err) - } - return string(bs), nil - - case []InputMedia: - for idx, item := range val { - err := item.InputParams(k+"_"+strconv.Itoa(idx), w) - if err != nil { - return "", fmt.Errorf("failed to read input multipart field: %w", err) - } + // In case of a simple inputfile attachment, rely on files + if inputFile, ok := val.(InputFile); ok { + return inputFile.getValue(), nil } + // For complex types (structs, maps, slices, etc.), marshal as JSON bs, err := json.Marshal(val) if err != nil { return "", fmt.Errorf("failed to marshal field %s to JSON: %w", k, err) } return string(bs), nil - case InputFile: - err := val.Attach(k, w) - if err != nil { - return "", fmt.Errorf("failed to attach field %s: %w", k, err) - } - return val.getValue(), nil - default: // For complex types (structs, maps, slices, etc.), marshal as JSON bs, err := json.Marshal(val) diff --git a/scripts/generate/gen.go b/scripts/generate/gen.go index da31bca..f87f07a 100644 --- a/scripts/generate/gen.go +++ b/scripts/generate/gen.go @@ -40,7 +40,26 @@ func receiver(n string) string { return strings.ToLower(string(rs)) } -func (td TypeDescription) sentByAPI(d APIDescription) bool { +func (td TypeDescription) sentAsArray(d APIDescription) bool { + for _, v := range d.Methods { + for _, f := range v.Fields { + t, err := f.getPreferredType(d) + if err != nil { + return false + } + s, ok := strings.CutPrefix(t, "[]") + if !ok { + continue + } + if s == toGoType(td.Name) { + return true + } + } + } + return false +} + +func (td TypeDescription) receivedFromAPI(d APIDescription) bool { checked := map[string]bool{} for _, m := range d.Methods { diff --git a/scripts/generate/methods.go b/scripts/generate/methods.go index ca009da..61e4a8d 100644 --- a/scripts/generate/methods.go +++ b/scripts/generate/methods.go @@ -294,6 +294,10 @@ func (fs Fields) getFunctionArgs(d APIDescription) ([]string, error) { return nil, fmt.Errorf("failed to get preferred type: %w", err) } + if coreType, isArray := strings.CutPrefix(fieldType, "[]"); isArray && isTgType(d, coreType) && needsArrayAttachment(d, d.Types[coreType]) { + fieldType = d.Types[coreType].Name + "s" + } + args = append(args, fmt.Sprintf("%s %s", snakeToCamel(f.Name), fieldType)) } return args, nil diff --git a/scripts/generate/types.go b/scripts/generate/types.go index 569c942..81c007d 100644 --- a/scripts/generate/types.go +++ b/scripts/generate/types.go @@ -10,7 +10,7 @@ import ( ) var ( - inputParamsTmpl = template.Must(template.New("inputParamsMethod").Parse(inputParamsMethod)) + attachTmpl = template.Must(template.New("attachMethod").Parse(attachMethod)) customMarshalTmpl = template.Must(template.New("customMarshal").Parse(customMarshal)) customUnmarshalTmpl = template.Must(template.New("customUnmarshal").Parse(customUnmarshal)) customStructUnmarshalTmpl = template.Must(template.New("customStructUnmarshal").Parse(customStructUnmarshal)) @@ -28,6 +28,7 @@ import ( "encoding/json" "fmt" "mime/multipart" + "strconv" ) `) @@ -75,7 +76,7 @@ func generateTypeDef(d APIDescription, tgType TypeDescription) (string, error) { typeDef.WriteString("\n}") } - if tgType.sentByAPI(d) { + if tgType.receivedFromAPI(d) { customUnmarshalDef, err := setupCustomUnmarshal(d, tgType) if err != nil { return "", fmt.Errorf("failed to setup custom unmarshal for %s: %w", tgType.Name, err) @@ -89,25 +90,33 @@ func generateTypeDef(d APIDescription, tgType TypeDescription) (string, error) { } typeDef.WriteString(interfaces) - ok, fieldName, err := containsInputFile(d, tgType, map[string]bool{}) + ok, fieldName, err := findInputFile(d, tgType, map[string]bool{}) if err != nil { return "", fmt.Errorf("failed to check if type requires special handling: %w", err) } if ok { // TODO: Investigate if thumbnails need special handling too. - err = inputParamsTmpl.Execute(&typeDef, inputParamsMethodData{ + err = attachTmpl.Execute(&typeDef, attachMethodData{ Type: tgType.Name, Field: snakeToTitle(fieldName), Thumbnail: containsThumbnail(tgType), }) if err != nil { - return "", fmt.Errorf("failed to generate %s inputparam methods: %w", tgType.Name, err) + return "", fmt.Errorf("failed to generate %s attachment methods: %w", tgType.Name, err) } } + if needsArrayAttachment(d, tgType) { + typeDef.WriteString(generateAttachableListType(tgType)) + } + return typeDef.String(), nil } +func needsArrayAttachment(d APIDescription, tgType TypeDescription) bool { + return tgType.sentAsArray(d) && containsInputFile(d, tgType) +} + func enforceTypeAssertion(name string, subtypes []TypeDescription) string { bd := strings.Builder{} bd.WriteString("\n// Ensure that all subtypes correctly implement the parent interface.") @@ -119,9 +128,21 @@ func enforceTypeAssertion(name string, subtypes []TypeDescription) string { return bd.String() } -// containsInputFile returns a boolean to indicate whether or not tgType contains an InputFile. +func containsInputFile(d APIDescription, tgType TypeDescription) bool { + for _, subtype := range tgType.Subtypes { + ok, _, _ := findInputFile(d, d.Types[subtype], map[string]bool{}) + if ok { + return true + } + } + + ok, _, _ := findInputFile(d, tgType, map[string]bool{}) + return ok +} + +// findInputFile returns a boolean to indicate whether or not tgType contains an InputFile. // If true, it also returns the field name of that inputfile. -func containsInputFile(d APIDescription, tgType TypeDescription, checked map[string]bool) (bool, string, error) { +func findInputFile(d APIDescription, tgType TypeDescription, checked map[string]bool) (bool, string, error) { // If already checked, we don't need to check again. This avoids infinite recursive loops. if checked[tgType.Name] { return false, "", nil @@ -143,7 +164,7 @@ func containsInputFile(d APIDescription, tgType TypeDescription, checked map[str } if isTgType(d, goType) { - ok, _, err := containsInputFile(d, d.Types[goType], checked) + ok, _, err := findInputFile(d, d.Types[goType], checked) if err != nil { return false, "", fmt.Errorf("failed to check if %s contains inputfiles: %w", goType, err) } @@ -180,9 +201,13 @@ func generateParentType(d APIDescription, tgType TypeDescription) (string, error typeDef.WriteString(tgType.docs()) typeDef.WriteString(interfaceDefinition) - // If an interface type is sent by the API (eg in an update, or as a return) then we need to define custom + if needsArrayAttachment(d, tgType) { + typeDef.WriteString(generateAttachableListType(tgType)) + } + + // If an interface type is received from the API (eg in an update, or as a return) then we need to define custom // UnmarshalJSON methods to handle those edge cases into the right structs. - if len(tgType.Subtypes) > 0 && tgType.sentByAPI(d) { + if len(tgType.Subtypes) > 0 && tgType.receivedFromAPI(d) { unmarshalFunc, err := interfaceUnmarshalFunc(d, tgType) if err != nil { return "", fmt.Errorf("unable to generate interface unmarshal function: %w", err) @@ -195,6 +220,24 @@ func generateParentType(d APIDescription, tgType TypeDescription) (string, error return typeDef.String(), nil } +func generateAttachableListType(tgType TypeDescription) string { + pluralName := tgType.Name + "s" + return fmt.Sprintf(` +type %s []%s + +func (ts %s) Attach(k string, w *multipart.Writer) error { + for idx, item := range ts { + err := item.Attach(k+"_"+strconv.Itoa(idx), w) + if err != nil { + return fmt.Errorf("failed to attach to multipart field: %%w", err) + } + } + + return nil +} +`, pluralName, tgType.Name, pluralName) +} + // Incoming types which marshal into interfaces need special handling to make sure the interfaces are // populated correctly. func setupCustomUnmarshal(d APIDescription, tgType TypeDescription) (string, error) { @@ -459,7 +502,7 @@ func generateGenericInterfaceType(d APIDescription, name string, subtypes []Type return "", fmt.Errorf("failed to get constant fields: %w", err) } - hasInputFile, fieldName, err := containsInputFile(d, subtypes[0], map[string]bool{}) + hasInputFile, fieldName, err := findInputFile(d, subtypes[0], map[string]bool{}) if err != nil { return "", fmt.Errorf("failed to check if %s types all contain inputfiles: %w", name, err) } @@ -477,8 +520,8 @@ func generateGenericInterfaceType(d APIDescription, name string, subtypes []Type bd.WriteString(fmt.Sprintf("\nGet%s() %s", snakeToTitle(f.Name), prefType)) } if hasInputFile { - bd.WriteString("\n// InputParams allows for uploading attachments with files.") - bd.WriteString("\nInputParams(string, *multipart.Writer) error") + bd.WriteString("\n// Attach allows for uploading attachments with files.") + bd.WriteString("\nAttach") } // Only require merge funcs when there are common fields, one is a constant, and all types match across types. @@ -744,14 +787,14 @@ func (v {{.Type}}) MarshalJSON() ([]byte, error) { } ` -type inputParamsMethodData struct { +type attachMethodData struct { Type string Field string Thumbnail bool } -const inputParamsMethod = ` -func (v {{.Type}}) InputParams(mediaName string, w *multipart.Writer) error { +const attachMethod = ` +func (v {{.Type}}) Attach(mediaName string, w *multipart.Writer) error { if v.{{.Field}} != nil { err := v.{{.Field}}.Attach(mediaName, w) if err != nil { From b197a1daa41b52daf3a7d4beb300f6a2662369b7 Mon Sep 17 00:00:00 2001 From: Paul Larsen Date: Sun, 1 Mar 2026 23:17:49 +0100 Subject: [PATCH 2/4] regenerate --- gen_methods.go | 12 +++++----- gen_types.go | 64 ++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/gen_methods.go b/gen_methods.go index a562a3b..7d32b15 100755 --- a/gen_methods.go +++ b/gen_methods.go @@ -946,12 +946,12 @@ type CreateNewStickerSetOpts struct { // - title (type string): Sticker set title, 1-64 characters // - stickers (type []InputSticker): A JSON-serialized list of 1-50 initial stickers to be added to the sticker set // - opts (type CreateNewStickerSetOpts): All optional parameters. -func (bot *Bot) CreateNewStickerSet(userId int64, name string, title string, stickers []InputSticker, opts *CreateNewStickerSetOpts) (bool, error) { +func (bot *Bot) CreateNewStickerSet(userId int64, name string, title string, stickers InputStickers, opts *CreateNewStickerSetOpts) (bool, error) { return bot.CreateNewStickerSetWithContext(context.Background(), userId, name, title, stickers, opts) } // CreateNewStickerSetWithContext is the same as Bot.CreateNewStickerSet, but with a context.Context parameter -func (bot *Bot) CreateNewStickerSetWithContext(ctx context.Context, userId int64, name string, title string, stickers []InputSticker, opts *CreateNewStickerSetOpts) (bool, error) { +func (bot *Bot) CreateNewStickerSetWithContext(ctx context.Context, userId int64, name string, title string, stickers InputStickers, opts *CreateNewStickerSetOpts) (bool, error) { v := map[string]any{} v["user_id"] = userId v["name"] = name @@ -5073,12 +5073,12 @@ type SendMediaGroupOpts struct { // - chatId (type int64): Unique identifier for the target chat // - media (type []InputMedia): A JSON-serialized array describing messages to be sent, must include 2-10 items // - opts (type SendMediaGroupOpts): All optional parameters. -func (bot *Bot) SendMediaGroup(chatId int64, media []InputMedia, opts *SendMediaGroupOpts) ([]Message, error) { +func (bot *Bot) SendMediaGroup(chatId int64, media InputMedias, opts *SendMediaGroupOpts) ([]Message, error) { return bot.SendMediaGroupWithContext(context.Background(), chatId, media, opts) } // SendMediaGroupWithContext is the same as Bot.SendMediaGroup, but with a context.Context parameter -func (bot *Bot) SendMediaGroupWithContext(ctx context.Context, chatId int64, media []InputMedia, opts *SendMediaGroupOpts) ([]Message, error) { +func (bot *Bot) SendMediaGroupWithContext(ctx context.Context, chatId int64, media InputMedias, opts *SendMediaGroupOpts) ([]Message, error) { v := map[string]any{} v["chat_id"] = chatId v["media"] = media @@ -5274,12 +5274,12 @@ type SendPaidMediaOpts struct { // - starCount (type int64): The number of Telegram Stars that must be paid to buy access to the media; 1-25000 // - media (type []InputPaidMedia): A JSON-serialized array describing the media to be sent; up to 10 items // - opts (type SendPaidMediaOpts): All optional parameters. -func (bot *Bot) SendPaidMedia(chatId int64, starCount int64, media []InputPaidMedia, opts *SendPaidMediaOpts) (*Message, error) { +func (bot *Bot) SendPaidMedia(chatId int64, starCount int64, media InputPaidMedias, opts *SendPaidMediaOpts) (*Message, error) { return bot.SendPaidMediaWithContext(context.Background(), chatId, starCount, media, opts) } // SendPaidMediaWithContext is the same as Bot.SendPaidMedia, but with a context.Context parameter -func (bot *Bot) SendPaidMediaWithContext(ctx context.Context, chatId int64, starCount int64, media []InputPaidMedia, opts *SendPaidMediaOpts) (*Message, error) { +func (bot *Bot) SendPaidMediaWithContext(ctx context.Context, chatId int64, starCount int64, media InputPaidMedias, opts *SendPaidMediaOpts) (*Message, error) { v := map[string]any{} v["chat_id"] = chatId v["star_count"] = starCount diff --git a/gen_types.go b/gen_types.go index bf666e3..c834b56 100755 --- a/gen_types.go +++ b/gen_types.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "mime/multipart" + "strconv" ) type ReplyMarkup interface { @@ -5050,8 +5051,8 @@ func (v InputLocationMessageContent) inputMessageContent() {} type InputMedia interface { GetType() string GetMedia() InputFileOrString - // InputParams allows for uploading attachments with files. - InputParams(string, *multipart.Writer) error + // Attach allows for uploading attachments with files. + Attach // MergeInputMedia returns a MergedInputMedia struct to simplify working with complex telegram types in a non-generic world. MergeInputMedia() MergedInputMedia // inputMedia exists to avoid external types implementing this interface. @@ -5123,6 +5124,19 @@ func (v MergedInputMedia) MergeInputMedia() MergedInputMedia { return v } +type InputMedias []InputMedia + +func (ts InputMedias) Attach(k string, w *multipart.Writer) error { + for idx, item := range ts { + err := item.Attach(k+"_"+strconv.Itoa(idx), w) + if err != nil { + return fmt.Errorf("failed to attach to multipart field: %w", err) + } + } + + return nil +} + // InputMediaAnimation (https://core.telegram.org/bots/api#inputmediaanimation) // // Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent. @@ -5192,7 +5206,7 @@ func (v InputMediaAnimation) MarshalJSON() ([]byte, error) { // InputMediaAnimation.inputMedia is a dummy method to avoid interface implementation. func (v InputMediaAnimation) inputMedia() {} -func (v InputMediaAnimation) InputParams(mediaName string, w *multipart.Writer) error { +func (v InputMediaAnimation) Attach(mediaName string, w *multipart.Writer) error { if v.Media != nil { err := v.Media.Attach(mediaName, w) if err != nil { @@ -5273,7 +5287,7 @@ func (v InputMediaAudio) MarshalJSON() ([]byte, error) { // InputMediaAudio.inputMedia is a dummy method to avoid interface implementation. func (v InputMediaAudio) inputMedia() {} -func (v InputMediaAudio) InputParams(mediaName string, w *multipart.Writer) error { +func (v InputMediaAudio) Attach(mediaName string, w *multipart.Writer) error { if v.Media != nil { err := v.Media.Attach(mediaName, w) if err != nil { @@ -5348,7 +5362,7 @@ func (v InputMediaDocument) MarshalJSON() ([]byte, error) { // InputMediaDocument.inputMedia is a dummy method to avoid interface implementation. func (v InputMediaDocument) inputMedia() {} -func (v InputMediaDocument) InputParams(mediaName string, w *multipart.Writer) error { +func (v InputMediaDocument) Attach(mediaName string, w *multipart.Writer) error { if v.Media != nil { err := v.Media.Attach(mediaName, w) if err != nil { @@ -5423,7 +5437,7 @@ func (v InputMediaPhoto) MarshalJSON() ([]byte, error) { // InputMediaPhoto.inputMedia is a dummy method to avoid interface implementation. func (v InputMediaPhoto) inputMedia() {} -func (v InputMediaPhoto) InputParams(mediaName string, w *multipart.Writer) error { +func (v InputMediaPhoto) Attach(mediaName string, w *multipart.Writer) error { if v.Media != nil { err := v.Media.Attach(mediaName, w) if err != nil { @@ -5512,7 +5526,7 @@ func (v InputMediaVideo) MarshalJSON() ([]byte, error) { // InputMediaVideo.inputMedia is a dummy method to avoid interface implementation. func (v InputMediaVideo) inputMedia() {} -func (v InputMediaVideo) InputParams(mediaName string, w *multipart.Writer) error { +func (v InputMediaVideo) Attach(mediaName string, w *multipart.Writer) error { if v.Media != nil { err := v.Media.Attach(mediaName, w) if err != nil { @@ -5560,8 +5574,8 @@ var ( type InputPaidMedia interface { GetType() string GetMedia() InputFileOrString - // InputParams allows for uploading attachments with files. - InputParams(string, *multipart.Writer) error + // Attach allows for uploading attachments with files. + Attach // MergeInputPaidMedia returns a MergedInputPaidMedia struct to simplify working with complex telegram types in a non-generic world. MergeInputPaidMedia() MergedInputPaidMedia // inputPaidMedia exists to avoid external types implementing this interface. @@ -5614,6 +5628,19 @@ func (v MergedInputPaidMedia) MergeInputPaidMedia() MergedInputPaidMedia { return v } +type InputPaidMedias []InputPaidMedia + +func (ts InputPaidMedias) Attach(k string, w *multipart.Writer) error { + for idx, item := range ts { + err := item.Attach(k+"_"+strconv.Itoa(idx), w) + if err != nil { + return fmt.Errorf("failed to attach to multipart field: %w", err) + } + } + + return nil +} + // InputPaidMediaPhoto (https://core.telegram.org/bots/api#inputpaidmediaphoto) // // The paid media to send is a photo. @@ -5656,7 +5683,7 @@ func (v InputPaidMediaPhoto) MarshalJSON() ([]byte, error) { // InputPaidMediaPhoto.inputPaidMedia is a dummy method to avoid interface implementation. func (v InputPaidMediaPhoto) inputPaidMedia() {} -func (v InputPaidMediaPhoto) InputParams(mediaName string, w *multipart.Writer) error { +func (v InputPaidMediaPhoto) Attach(mediaName string, w *multipart.Writer) error { if v.Media != nil { err := v.Media.Attach(mediaName, w) if err != nil { @@ -5730,7 +5757,7 @@ func (v InputPaidMediaVideo) MarshalJSON() ([]byte, error) { // InputPaidMediaVideo.inputPaidMedia is a dummy method to avoid interface implementation. func (v InputPaidMediaVideo) inputPaidMedia() {} -func (v InputPaidMediaVideo) InputParams(mediaName string, w *multipart.Writer) error { +func (v InputPaidMediaVideo) Attach(mediaName string, w *multipart.Writer) error { if v.Media != nil { err := v.Media.Attach(mediaName, w) if err != nil { @@ -5897,7 +5924,7 @@ type InputSticker struct { Keywords []string `json:"keywords,omitempty"` } -func (v InputSticker) InputParams(mediaName string, w *multipart.Writer) error { +func (v InputSticker) Attach(mediaName string, w *multipart.Writer) error { if v.Sticker != nil { err := v.Sticker.Attach(mediaName, w) if err != nil { @@ -5908,6 +5935,19 @@ func (v InputSticker) InputParams(mediaName string, w *multipart.Writer) error { return nil } +type InputStickers []InputSticker + +func (ts InputStickers) Attach(k string, w *multipart.Writer) error { + for idx, item := range ts { + err := item.Attach(k+"_"+strconv.Itoa(idx), w) + if err != nil { + return fmt.Errorf("failed to attach to multipart field: %w", err) + } + } + + return nil +} + // InputStoryContent (https://core.telegram.org/bots/api#inputstorycontent) // // This object describes the content of a story to post. Currently, it can be one of From 589915546e9eff1b1b1f1d54e64034bd9dbada46 Mon Sep 17 00:00:00 2001 From: Paul Larsen Date: Sun, 1 Mar 2026 23:20:14 +0100 Subject: [PATCH 3/4] add comment --- file.go | 1 + 1 file changed, 1 insertion(+) diff --git a/file.go b/file.go index e797fc5..dddcb8e 100644 --- a/file.go +++ b/file.go @@ -25,6 +25,7 @@ type InputFileOrString interface { getValue() string } +// Attach is the core interface to attach items to a multipart writer. It gets reused across InputFile, InputMedia, etc. type Attach interface { Attach(name string, w *multipart.Writer) error } From 0abe92d8e57a1e4ab66facf36592cb60a7845924 Mon Sep 17 00:00:00 2001 From: Paul Larsen Date: Sun, 1 Mar 2026 23:25:52 +0100 Subject: [PATCH 4/4] comment and cleanup --- scripts/generate/gen.go | 4 ++++ scripts/generate/methods.go | 4 +++- scripts/generate/types.go | 3 +-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/generate/gen.go b/scripts/generate/gen.go index f87f07a..5650021 100644 --- a/scripts/generate/gen.go +++ b/scripts/generate/gen.go @@ -29,6 +29,10 @@ func (td TypeDescription) receiverName() string { return receiver(td.Name) } +func (td TypeDescription) pluralisedName() string { + return td.Name + "s" +} + func receiver(n string) string { var rs []rune for _, r := range n { diff --git a/scripts/generate/methods.go b/scripts/generate/methods.go index 61e4a8d..bd97e37 100644 --- a/scripts/generate/methods.go +++ b/scripts/generate/methods.go @@ -295,7 +295,9 @@ func (fs Fields) getFunctionArgs(d APIDescription) ([]string, error) { } if coreType, isArray := strings.CutPrefix(fieldType, "[]"); isArray && isTgType(d, coreType) && needsArrayAttachment(d, d.Types[coreType]) { - fieldType = d.Types[coreType].Name + "s" + // If we're passing in a telegram-type array for a type which uses our Attach interface, then we should use our pluralised version of it. + // This implements the Attach interface on the underlying list items, simplifying our sending logic. + fieldType = d.Types[coreType].pluralisedName() } args = append(args, fmt.Sprintf("%s %s", snakeToCamel(f.Name), fieldType)) diff --git a/scripts/generate/types.go b/scripts/generate/types.go index 81c007d..8f1c819 100644 --- a/scripts/generate/types.go +++ b/scripts/generate/types.go @@ -221,7 +221,6 @@ func generateParentType(d APIDescription, tgType TypeDescription) (string, error } func generateAttachableListType(tgType TypeDescription) string { - pluralName := tgType.Name + "s" return fmt.Sprintf(` type %s []%s @@ -235,7 +234,7 @@ func (ts %s) Attach(k string, w *multipart.Writer) error { return nil } -`, pluralName, tgType.Name, pluralName) +`, tgType.pluralisedName(), tgType.Name, tgType.pluralisedName()) } // Incoming types which marshal into interfaces need special handling to make sure the interfaces are