Skip to content

Commit 3c735fe

Browse files
chore(internal): codegen related update
1 parent 92c49b3 commit 3c735fe

File tree

15 files changed

+171
-54
lines changed

15 files changed

+171
-54
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ $ ./scripts/lint
99

1010
This will install all the required dependencies and build the SDK.
1111

12-
You can also [install go 1.18+ manually](https://go.dev/doc/install).
12+
You can also [install go 1.22+ manually](https://go.dev/doc/install).
1313

1414
## Modifying/Adding code
1515

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# Beeper Desktop Go API Library
22

3+
<!-- x-release-please-start-version -->
4+
35
<a href="https://pkg.go.dev/github.com/beeper/desktop-api-go"><img src="https://pkg.go.dev/badge/github.com/beeper/desktop-api-go.svg" alt="Go Reference"></a>
46

7+
<!-- x-release-please-end -->
8+
59
The Beeper Desktop Go library provides convenient access to the [Beeper Desktop REST API](https://developers.beeper.com/desktop-api/)
610
from applications written in Go.
711

@@ -29,7 +33,7 @@ go get -u 'github.com/beeper/[email protected]'
2933

3034
## Requirements
3135

32-
This library requires Go 1.18+.
36+
This library requires Go 1.22+.
3337

3438
## Usage
3539

account.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package beeperdesktopapi
55
import (
66
"context"
77
"net/http"
8+
"slices"
89

910
"github.com/beeper/desktop-api-go/internal/apijson"
1011
"github.com/beeper/desktop-api-go/internal/requestconfig"
@@ -37,7 +38,7 @@ func NewAccountService(opts ...option.RequestOption) (r AccountService) {
3738
// Lists chat accounts across networks (WhatsApp, Telegram, Twitter/X, etc.)
3839
// actively connected to this Beeper Desktop instance
3940
func (r *AccountService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Account, err error) {
40-
opts = append(r.Options[:], opts...)
41+
opts = slices.Concat(r.Options, opts)
4142
path := "v1/accounts"
4243
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
4344
return

chat.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"net/http"
1111
"net/url"
12+
"slices"
1213
"time"
1314

1415
"github.com/beeper/desktop-api-go/internal/apijson"
@@ -48,15 +49,15 @@ func NewChatService(opts ...option.RequestOption) (r ChatService) {
4849
// Create a single or group chat on a specific account using participant IDs and
4950
// optional title.
5051
func (r *ChatService) New(ctx context.Context, body ChatNewParams, opts ...option.RequestOption) (res *ChatNewResponse, err error) {
51-
opts = append(r.Options[:], opts...)
52+
opts = slices.Concat(r.Options, opts)
5253
path := "v1/chats"
5354
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
5455
return
5556
}
5657

5758
// Retrieve chat details including metadata, participants, and latest message
5859
func (r *ChatService) Get(ctx context.Context, chatID string, query ChatGetParams, opts ...option.RequestOption) (res *Chat, err error) {
59-
opts = append(r.Options[:], opts...)
60+
opts = slices.Concat(r.Options, opts)
6061
if chatID == "" {
6162
err = errors.New("missing required chatID parameter")
6263
return
@@ -70,7 +71,7 @@ func (r *ChatService) Get(ctx context.Context, chatID string, query ChatGetParam
7071
// accounts into a single paginated list.
7172
func (r *ChatService) List(ctx context.Context, query ChatListParams, opts ...option.RequestOption) (res *pagination.CursorList[ChatListResponse], err error) {
7273
var raw *http.Response
73-
opts = append(r.Options[:], opts...)
74+
opts = slices.Concat(r.Options, opts)
7475
opts = append([]option.RequestOption{option.WithResponseInto(&raw)}, opts...)
7576
path := "v1/chats"
7677
cfg, err := requestconfig.NewRequestConfig(ctx, http.MethodGet, path, query, &res, opts...)
@@ -94,7 +95,7 @@ func (r *ChatService) ListAutoPaging(ctx context.Context, query ChatListParams,
9495
// Archive or unarchive a chat. Set archived=true to move to archive,
9596
// archived=false to move back to inbox
9697
func (r *ChatService) Archive(ctx context.Context, chatID string, body ChatArchiveParams, opts ...option.RequestOption) (res *shared.BaseResponse, err error) {
97-
opts = append(r.Options[:], opts...)
98+
opts = slices.Concat(r.Options, opts)
9899
if chatID == "" {
99100
err = errors.New("missing required chatID parameter")
100101
return
@@ -108,7 +109,7 @@ func (r *ChatService) Archive(ctx context.Context, chatID string, body ChatArchi
108109
// algorithm.
109110
func (r *ChatService) Search(ctx context.Context, query ChatSearchParams, opts ...option.RequestOption) (res *pagination.CursorSearch[Chat], err error) {
110111
var raw *http.Response
111-
opts = append(r.Options[:], opts...)
112+
opts = slices.Concat(r.Options, opts)
112113
opts = append([]option.RequestOption{option.WithResponseInto(&raw)}, opts...)
113114
path := "v1/chats/search"
114115
cfg, err := requestconfig.NewRequestConfig(ctx, http.MethodGet, path, query, &res, opts...)

chatreminder.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88
"fmt"
99
"net/http"
10+
"slices"
1011

1112
"github.com/beeper/desktop-api-go/internal/apijson"
1213
"github.com/beeper/desktop-api-go/internal/requestconfig"
@@ -38,7 +39,7 @@ func NewChatReminderService(opts ...option.RequestOption) (r ChatReminderService
3839

3940
// Set a reminder for a chat at a specific time
4041
func (r *ChatReminderService) New(ctx context.Context, chatID string, body ChatReminderNewParams, opts ...option.RequestOption) (res *shared.BaseResponse, err error) {
41-
opts = append(r.Options[:], opts...)
42+
opts = slices.Concat(r.Options, opts)
4243
if chatID == "" {
4344
err = errors.New("missing required chatID parameter")
4445
return
@@ -50,7 +51,7 @@ func (r *ChatReminderService) New(ctx context.Context, chatID string, body ChatR
5051

5152
// Clear an existing reminder from a chat
5253
func (r *ChatReminderService) Delete(ctx context.Context, chatID string, opts ...option.RequestOption) (res *shared.BaseResponse, err error) {
53-
opts = append(r.Options[:], opts...)
54+
opts = slices.Concat(r.Options, opts)
5455
if chatID == "" {
5556
err = errors.New("missing required chatID parameter")
5657
return

client.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"context"
77
"net/http"
88
"os"
9+
"slices"
910

1011
"github.com/beeper/desktop-api-go/internal/requestconfig"
1112
"github.com/beeper/desktop-api-go/option"
@@ -88,7 +89,7 @@ func NewClient(opts ...option.RequestOption) (r Client) {
8889
// For even greater flexibility, see [option.WithResponseInto] and
8990
// [option.WithResponseBodyInto].
9091
func (r *Client) Execute(ctx context.Context, method string, path string, params any, res any, opts ...option.RequestOption) error {
91-
opts = append(r.Options, opts...)
92+
opts = slices.Concat(r.Options, opts)
9293
return requestconfig.ExecuteNewRequest(ctx, method, path, params, res, opts...)
9394
}
9495

@@ -128,7 +129,7 @@ func (r *Client) Delete(ctx context.Context, path string, params any, res any, o
128129
// Download a Matrix asset using its mxc:// or localmxc:// URL and return the local
129130
// file URL.
130131
func (r *Client) DownloadAsset(ctx context.Context, body DownloadAssetParams, opts ...option.RequestOption) (res *DownloadAssetResponse, err error) {
131-
opts = append(r.Options[:], opts...)
132+
opts = slices.Concat(r.Options, opts)
132133
path := "v1/download-asset"
133134
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
134135
return
@@ -137,7 +138,7 @@ func (r *Client) DownloadAsset(ctx context.Context, body DownloadAssetParams, op
137138
// Open Beeper Desktop and optionally navigate to a specific chat, message, or
138139
// pre-fill draft text and attachment.
139140
func (r *Client) Open(ctx context.Context, body OpenParams, opts ...option.RequestOption) (res *OpenResponse, err error) {
140-
opts = append(r.Options[:], opts...)
141+
opts = slices.Concat(r.Options, opts)
141142
path := "v1/open"
142143
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
143144
return
@@ -147,7 +148,7 @@ func (r *Client) Open(ctx context.Context, body OpenParams, opts ...option.Reque
147148
// of messages in one call. Paginate messages via search-messages. Paginate chats
148149
// via search-chats. Uses the same sorting as the chat search in the app.
149150
func (r *Client) Search(ctx context.Context, query SearchParams, opts ...option.RequestOption) (res *SearchResponse, err error) {
150-
opts = append(r.Options[:], opts...)
151+
opts = slices.Concat(r.Options, opts)
151152
path := "v1/search"
152153
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
153154
return

contact.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"context"
77
"net/http"
88
"net/url"
9+
"slices"
910

1011
"github.com/beeper/desktop-api-go/internal/apijson"
1112
"github.com/beeper/desktop-api-go/internal/apiquery"
@@ -39,7 +40,7 @@ func NewContactService(opts ...option.RequestOption) (r ContactService) {
3940
// Search contacts across on a specific account using the network's search API.
4041
// Only use for creating new chats.
4142
func (r *ContactService) Search(ctx context.Context, query ContactSearchParams, opts ...option.RequestOption) (res *ContactSearchResponse, err error) {
42-
opts = append(r.Options[:], opts...)
43+
opts = slices.Concat(r.Options, opts)
4344
path := "v1/contacts/search"
4445
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
4546
return

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/beeper/desktop-api-go
22

3-
go 1.21
3+
go 1.22
44

55
require (
66
github.com/tidwall/gjson v1.14.4

internal/apijson/decodeparam_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,36 @@ func init() {
351351
})
352352
}
353353

354+
type FooVariant struct {
355+
Type string `json:"type,required"`
356+
Value string `json:"value,required"`
357+
}
358+
359+
type BarVariant struct {
360+
Type string `json:"type,required"`
361+
Enable bool `json:"enable,required"`
362+
}
363+
364+
type MultiDiscriminatorUnion struct {
365+
OfFoo *FooVariant `json:",inline"`
366+
OfBar *BarVariant `json:",inline"`
367+
368+
paramUnion
369+
}
370+
371+
func init() {
372+
apijson.RegisterDiscriminatedUnion[MultiDiscriminatorUnion]("type", map[string]reflect.Type{
373+
"foo": reflect.TypeOf(FooVariant{}),
374+
"foo_v2": reflect.TypeOf(FooVariant{}),
375+
"bar": reflect.TypeOf(BarVariant{}),
376+
"bar_legacy": reflect.TypeOf(BarVariant{}),
377+
})
378+
}
379+
380+
func (m *MultiDiscriminatorUnion) UnmarshalJSON(data []byte) error {
381+
return apijson.UnmarshalRoot(data, m)
382+
}
383+
354384
func (d *DiscriminatedUnion) UnmarshalJSON(data []byte) error {
355385
return apijson.UnmarshalRoot(data, d)
356386
}
@@ -408,3 +438,61 @@ func TestDiscriminatedUnion(t *testing.T) {
408438
})
409439
}
410440
}
441+
442+
func TestMultiDiscriminatorUnion(t *testing.T) {
443+
tests := map[string]struct {
444+
raw string
445+
target MultiDiscriminatorUnion
446+
shouldFail bool
447+
}{
448+
"foo_variant": {
449+
raw: `{"type":"foo","value":"test"}`,
450+
target: MultiDiscriminatorUnion{OfFoo: &FooVariant{
451+
Type: "foo",
452+
Value: "test",
453+
}},
454+
},
455+
"foo_v2_variant": {
456+
raw: `{"type":"foo_v2","value":"test_v2"}`,
457+
target: MultiDiscriminatorUnion{OfFoo: &FooVariant{
458+
Type: "foo_v2",
459+
Value: "test_v2",
460+
}},
461+
},
462+
"bar_variant": {
463+
raw: `{"type":"bar","enable":true}`,
464+
target: MultiDiscriminatorUnion{OfBar: &BarVariant{
465+
Type: "bar",
466+
Enable: true,
467+
}},
468+
},
469+
"bar_legacy_variant": {
470+
raw: `{"type":"bar_legacy","enable":false}`,
471+
target: MultiDiscriminatorUnion{OfBar: &BarVariant{
472+
Type: "bar_legacy",
473+
Enable: false,
474+
}},
475+
},
476+
"invalid_type": {
477+
raw: `{"type":"unknown","value":"test"}`,
478+
target: MultiDiscriminatorUnion{},
479+
shouldFail: true,
480+
},
481+
}
482+
483+
for name, test := range tests {
484+
t.Run(name, func(t *testing.T) {
485+
var dst MultiDiscriminatorUnion
486+
err := json.Unmarshal([]byte(test.raw), &dst)
487+
if err != nil && !test.shouldFail {
488+
t.Fatalf("failed unmarshal with err: %v", err)
489+
}
490+
if err == nil && test.shouldFail {
491+
t.Fatalf("expected unmarshal to fail but it succeeded")
492+
}
493+
if !reflect.DeepEqual(dst, test.target) {
494+
t.Fatalf("failed equality, got %#v but expected %#v", dst, test.target)
495+
}
496+
})
497+
}
498+
}

internal/apijson/encoder.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import (
1616

1717
var encoders sync.Map // map[encoderEntry]encoderFunc
1818

19+
// If we want to set a literal key value into JSON using sjson, we need to make sure it doesn't have
20+
// special characters that sjson interprets as a path.
21+
var EscapeSJSONKey = strings.NewReplacer("\\", "\\\\", "|", "\\|", "#", "\\#", "@", "\\@", "*", "\\*", ".", "\\.", ":", "\\:", "?", "\\?").Replace
22+
1923
func Marshal(value any) ([]byte, error) {
2024
e := &encoder{dateFormat: time.RFC3339}
2125
return e.marshal(value)
@@ -270,7 +274,7 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc {
270274
if encoded == nil {
271275
continue
272276
}
273-
json, err = sjson.SetRawBytes(json, ef.tag.name, encoded)
277+
json, err = sjson.SetRawBytes(json, EscapeSJSONKey(ef.tag.name), encoded)
274278
if err != nil {
275279
return nil, err
276280
}
@@ -348,7 +352,7 @@ func (e *encoder) encodeMapEntries(json []byte, v reflect.Value) ([]byte, error)
348352
}
349353
encodedKeyString = string(encodedKeyBytes)
350354
}
351-
encodedKey := []byte(sjsonReplacer.Replace(encodedKeyString))
355+
encodedKey := []byte(encodedKeyString)
352356
pairs = append(pairs, mapPair{key: encodedKey, value: iter.Value()})
353357
}
354358

@@ -366,7 +370,7 @@ func (e *encoder) encodeMapEntries(json []byte, v reflect.Value) ([]byte, error)
366370
if len(encodedValue) == 0 {
367371
continue
368372
}
369-
json, err = sjson.SetRawBytes(json, string(p.key), encodedValue)
373+
json, err = sjson.SetRawBytes(json, EscapeSJSONKey(string(p.key)), encodedValue)
370374
if err != nil {
371375
return nil, err
372376
}
@@ -386,7 +390,3 @@ func (e *encoder) newMapEncoder(_ reflect.Type) encoderFunc {
386390
return json, nil
387391
}
388392
}
389-
390-
// If we want to set a literal key value into JSON using sjson, we need to make sure it doesn't have
391-
// special characters that sjson interprets as a path.
392-
var sjsonReplacer *strings.Replacer = strings.NewReplacer(".", "\\.", ":", "\\:", "*", "\\*")

0 commit comments

Comments
 (0)