Skip to content

Commit 1e55295

Browse files
committed
feat: allow custom json encoder/decoder.
The encoding/json package in the standard library isn't particularly performant, and there have been many first-party and third-party efforts to improve it. This patch allows users of the sdk to configure a custom json codec to marshall and unmarshall json with their tools of choice. Note: we can update to encoding/json/v2 as the default once it's marked stable in a future golang release.
1 parent 1866144 commit 1e55295

8 files changed

Lines changed: 379 additions & 285 deletions

File tree

internal/generate/templates/no_resptype_body_method.tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
}{{end}}{{if .IsAppJSON}}
55
// Encode the request body as json.
66
b := new(bytes.Buffer)
7-
if err := json.NewEncoder(b).Encode(params.Body); err != nil {
7+
if err := c.newJSONEncoder(b).Encode(params.Body); err != nil {
88
return fmt.Errorf("encoding json body request failed: %v", err)
99
}{{else}}
1010
b := params.Body{{end}}

internal/generate/templates/resptype_body_method.tpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
}{{end}}{{if .IsAppJSON}}
55
// Encode the request body as json.
66
b := new(bytes.Buffer)
7-
if err := json.NewEncoder(b).Encode(params.Body); err != nil {
7+
if err := c.newJSONEncoder(b).Encode(params.Body); err != nil {
88
return nil, fmt.Errorf("encoding json body request failed: %v", err)
99
}{{else}}
1010
b := params.Body{{end}}
@@ -44,7 +44,7 @@
4444
}
4545

4646
var body {{.ResponseType}}
47-
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
47+
if err := c.newJSONDecoder(resp.Body).Decode(&body); err != nil {
4848
return nil, fmt.Errorf("error decoding response body: %v", err)
4949
}
5050

internal/generate/templates/resptype_method.tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
}
3838

3939
var body {{.ResponseType}}
40-
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
40+
if err := c.newJSONDecoder(resp.Body).Decode(&body); err != nil {
4141
return nil, fmt.Errorf("error decoding response body: %v", err)
4242
}
4343

internal/generate/test_utils/paths_output

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func (c *Client) IpPoolList(ctx context.Context, params IpPoolListParams, ) (*Ip
4949
}
5050

5151
var body IpPoolResultsPage
52-
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
52+
if err := c.newJSONDecoder(resp.Body).Decode(&body); err != nil {
5353
return nil, fmt.Errorf("error decoding response body: %v", err)
5454
}
5555

@@ -90,7 +90,7 @@ func (c *Client) IpPoolCreate(ctx context.Context, params IpPoolCreateParams, )
9090
}
9191
// Encode the request body as json.
9292
b := new(bytes.Buffer)
93-
if err := json.NewEncoder(b).Encode(params.Body); err != nil {
93+
if err := c.newJSONEncoder(b).Encode(params.Body); err != nil {
9494
return nil, fmt.Errorf("encoding json body request failed: %v", err)
9595
}
9696

@@ -127,7 +127,7 @@ func (c *Client) IpPoolCreate(ctx context.Context, params IpPoolCreateParams, )
127127
}
128128

129129
var body IpPool
130-
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
130+
if err := c.newJSONDecoder(resp.Body).Decode(&body); err != nil {
131131
return nil, fmt.Errorf("error decoding response body: %v", err)
132132
}
133133

@@ -174,7 +174,7 @@ func (c *Client) IpPoolView(ctx context.Context, params IpPoolViewParams, ) (*Ip
174174
}
175175

176176
var body IpPool
177-
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
177+
if err := c.newJSONDecoder(resp.Body).Decode(&body); err != nil {
178178
return nil, fmt.Errorf("error decoding response body: %v", err)
179179
}
180180

@@ -189,7 +189,7 @@ func (c *Client) IpPoolUpdate(ctx context.Context, params IpPoolUpdateParams, )
189189
}
190190
// Encode the request body as json.
191191
b := new(bytes.Buffer)
192-
if err := json.NewEncoder(b).Encode(params.Body); err != nil {
192+
if err := c.newJSONEncoder(b).Encode(params.Body); err != nil {
193193
return nil, fmt.Errorf("encoding json body request failed: %v", err)
194194
}
195195

@@ -227,7 +227,7 @@ func (c *Client) IpPoolUpdate(ctx context.Context, params IpPoolUpdateParams, )
227227
}
228228

229229
var body IpPool
230-
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
230+
if err := c.newJSONDecoder(resp.Body).Decode(&body); err != nil {
231231
return nil, fmt.Errorf("error decoding response body: %v", err)
232232
}
233233

internal/generate/test_utils/paths_output_expected

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func (c *Client) IpPoolList(ctx context.Context, params IpPoolListParams, ) (*Ip
4949
}
5050

5151
var body IpPoolResultsPage
52-
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
52+
if err := c.newJSONDecoder(resp.Body).Decode(&body); err != nil {
5353
return nil, fmt.Errorf("error decoding response body: %v", err)
5454
}
5555

@@ -90,7 +90,7 @@ func (c *Client) IpPoolCreate(ctx context.Context, params IpPoolCreateParams, )
9090
}
9191
// Encode the request body as json.
9292
b := new(bytes.Buffer)
93-
if err := json.NewEncoder(b).Encode(params.Body); err != nil {
93+
if err := c.newJSONEncoder(b).Encode(params.Body); err != nil {
9494
return nil, fmt.Errorf("encoding json body request failed: %v", err)
9595
}
9696

@@ -127,7 +127,7 @@ func (c *Client) IpPoolCreate(ctx context.Context, params IpPoolCreateParams, )
127127
}
128128

129129
var body IpPool
130-
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
130+
if err := c.newJSONDecoder(resp.Body).Decode(&body); err != nil {
131131
return nil, fmt.Errorf("error decoding response body: %v", err)
132132
}
133133

@@ -174,7 +174,7 @@ func (c *Client) IpPoolView(ctx context.Context, params IpPoolViewParams, ) (*Ip
174174
}
175175

176176
var body IpPool
177-
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
177+
if err := c.newJSONDecoder(resp.Body).Decode(&body); err != nil {
178178
return nil, fmt.Errorf("error decoding response body: %v", err)
179179
}
180180

@@ -189,7 +189,7 @@ func (c *Client) IpPoolUpdate(ctx context.Context, params IpPoolUpdateParams, )
189189
}
190190
// Encode the request body as json.
191191
b := new(bytes.Buffer)
192-
if err := json.NewEncoder(b).Encode(params.Body); err != nil {
192+
if err := c.newJSONEncoder(b).Encode(params.Body); err != nil {
193193
return nil, fmt.Errorf("encoding json body request failed: %v", err)
194194
}
195195

@@ -227,7 +227,7 @@ func (c *Client) IpPoolUpdate(ctx context.Context, params IpPoolUpdateParams, )
227227
}
228228

229229
var body IpPool
230-
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
230+
if err := c.newJSONDecoder(resp.Body).Decode(&body); err != nil {
231231
return nil, fmt.Errorf("error decoding response body: %v", err)
232232
}
233233

oxide/lib.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package oxide
66

77
import (
88
"context"
9+
"encoding/json"
910
"errors"
1011
"fmt"
1112
"io"
@@ -40,6 +41,37 @@ const (
4041
defaultConfigDir = ".config" + string(filepath.Separator) + "oxide"
4142
)
4243

44+
// JSONEncoder is an interface for encoding values to an output stream.
45+
type JSONEncoder interface {
46+
Encode(v any) error
47+
}
48+
49+
// JSONDecoder is an interface for decoding values from an input stream.
50+
type JSONDecoder interface {
51+
Decode(v any) error
52+
}
53+
54+
// JSONCodec is an interface that provides methods for creating JSON encoders and decoders.
55+
type JSONCodec interface {
56+
NewEncoder(w io.Writer) JSONEncoder
57+
NewDecoder(r io.Reader) JSONDecoder
58+
}
59+
60+
func defaultJSONCodec() JSONCodec {
61+
return stdJSONCodec{}
62+
}
63+
64+
// stdJSONCodec is the default implementation using the standard library.
65+
type stdJSONCodec struct{}
66+
67+
func (stdJSONCodec) NewEncoder(w io.Writer) JSONEncoder {
68+
return json.NewEncoder(w)
69+
}
70+
71+
func (stdJSONCodec) NewDecoder(r io.Reader) JSONDecoder {
72+
return json.NewDecoder(r)
73+
}
74+
4375
// Config is the configuration that can be set on a Client.
4476
type Config struct {
4577
// Base URL of the Oxide API including the scheme. For example,
@@ -69,6 +101,10 @@ type Config struct {
69101
// config.toml file for authentication. Will be overridden by
70102
// the Profile field.
71103
UseDefaultProfile bool
104+
105+
// A custom JSON codec for encoding and decoding. If not provided,
106+
// the standard library's json.NewEncoder and json.NewDecoder will be used.
107+
JSONCodec JSONCodec
72108
}
73109

74110
// Client which conforms to the OpenAPI3 specification for this service.
@@ -85,6 +121,9 @@ type Client struct {
85121

86122
// The user agent string to add to every API request.
87123
userAgent string
124+
125+
// JSON codec for encoding and decoding.
126+
jsonCodec JSONCodec
88127
}
89128

90129
type authCredentials struct {
@@ -106,6 +145,7 @@ func NewClient(cfg *Config) (*Client, error) {
106145
profile := os.Getenv(ProfileEnvVar)
107146
useDefaultProfile := false
108147
userAgent := defaultUserAgent()
148+
jsonCodec := defaultJSONCodec()
109149
httpClient := &http.Client{
110150
Timeout: 600 * time.Second,
111151
}
@@ -138,6 +178,10 @@ func NewClient(cfg *Config) (*Client, error) {
138178
if cfg.HTTPClient != nil {
139179
httpClient = cfg.HTTPClient
140180
}
181+
182+
if cfg.JSONCodec != nil {
183+
jsonCodec = cfg.JSONCodec
184+
}
141185
}
142186

143187
if (profile != "" || useDefaultProfile) && (host != "" || token != "") {
@@ -187,6 +231,7 @@ func NewClient(cfg *Config) (*Client, error) {
187231
host: host,
188232
userAgent: userAgent,
189233
client: httpClient,
234+
jsonCodec: jsonCodec,
190235
}
191236

192237
return client, nil
@@ -291,6 +336,16 @@ func parseBaseURL(baseURL string) (string, error) {
291336
return b, nil
292337
}
293338

339+
// newJSONEncoder creates a new JSON encoder for the given writer using the configured codec.
340+
func (c *Client) newJSONEncoder(w io.Writer) JSONEncoder {
341+
return c.jsonCodec.NewEncoder(w)
342+
}
343+
344+
// newJSONDecoder creates a new JSON decoder for the given reader using the configured codec.
345+
func (c *Client) newJSONDecoder(r io.Reader) JSONDecoder {
346+
return c.jsonCodec.NewDecoder(r)
347+
}
348+
294349
// buildRequest creates an HTTP request to interact with the Oxide API.
295350
func (c *Client) buildRequest(ctx context.Context, body io.Reader, method, uri string, params, queries map[string]string) (*http.Request, error) {
296351
// Create the request.

oxide/lib_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ import (
2222
"github.com/stretchr/testify/require"
2323
)
2424

25+
// customJSONCodec is a custom JSON codec for testing custom codec functionality.
26+
type customJSONCodec struct{}
27+
28+
func (customJSONCodec) NewEncoder(w io.Writer) JSONEncoder {
29+
return json.NewEncoder(w)
30+
}
31+
32+
func (customJSONCodec) NewDecoder(r io.Reader) JSONDecoder {
33+
return json.NewDecoder(r)
34+
}
35+
2536
func Test_buildRequest(t *testing.T) {
2637
t.Parallel()
2738

@@ -189,6 +200,7 @@ func Test_NewClient(t *testing.T) {
189200
Timeout: 600 * time.Second,
190201
},
191202
userAgent: defaultUserAgent(),
203+
jsonCodec: defaultJSONCodec(),
192204
},
193205
},
194206
"succeeds with valid client from env": {
@@ -203,6 +215,7 @@ func Test_NewClient(t *testing.T) {
203215
Timeout: 600 * time.Second,
204216
},
205217
userAgent: defaultUserAgent(),
218+
jsonCodec: defaultJSONCodec(),
206219
},
207220
},
208221
"succeeds with valid client from env and config": {
@@ -225,6 +238,7 @@ func Test_NewClient(t *testing.T) {
225238
Timeout: 500 * time.Second,
226239
},
227240
userAgent: "bob",
241+
jsonCodec: defaultJSONCodec(),
228242
},
229243
},
230244
"succeeds with config, overrides env": {
@@ -245,6 +259,7 @@ func Test_NewClient(t *testing.T) {
245259
Timeout: 600 * time.Second,
246260
},
247261
userAgent: defaultUserAgent(),
262+
jsonCodec: defaultJSONCodec(),
248263
},
249264
},
250265
"succeeds with profile": {
@@ -261,6 +276,7 @@ func Test_NewClient(t *testing.T) {
261276
Timeout: 600 * time.Second,
262277
},
263278
userAgent: defaultUserAgent(),
279+
jsonCodec: defaultJSONCodec(),
264280
},
265281
},
266282
"succeeds with profile from env": {
@@ -278,6 +294,7 @@ func Test_NewClient(t *testing.T) {
278294
Timeout: 600 * time.Second,
279295
},
280296
userAgent: defaultUserAgent(),
297+
jsonCodec: defaultJSONCodec(),
281298
},
282299
},
283300
"succeeds with default profile": {
@@ -294,6 +311,7 @@ func Test_NewClient(t *testing.T) {
294311
Timeout: 600 * time.Second,
295312
},
296313
userAgent: defaultUserAgent(),
314+
jsonCodec: defaultJSONCodec(),
297315
},
298316
},
299317
"succeeds with config dir and default profile": {
@@ -310,6 +328,7 @@ func Test_NewClient(t *testing.T) {
310328
Timeout: 600 * time.Second,
311329
},
312330
userAgent: defaultUserAgent(),
331+
jsonCodec: defaultJSONCodec(),
313332
},
314333
},
315334
"succeeds with config dir and profile": {
@@ -326,6 +345,7 @@ func Test_NewClient(t *testing.T) {
326345
Timeout: 600 * time.Second,
327346
},
328347
userAgent: defaultUserAgent(),
348+
jsonCodec: defaultJSONCodec(),
329349
},
330350
},
331351
"succeeds with profile, overrides env": {
@@ -346,6 +366,7 @@ func Test_NewClient(t *testing.T) {
346366
Timeout: 600 * time.Second,
347367
},
348368
userAgent: defaultUserAgent(),
369+
jsonCodec: defaultJSONCodec(),
349370
},
350371
},
351372
"succeeds with host and token from different sources ": {
@@ -364,6 +385,25 @@ func Test_NewClient(t *testing.T) {
364385
Timeout: 600 * time.Second,
365386
},
366387
userAgent: defaultUserAgent(),
388+
jsonCodec: defaultJSONCodec(),
389+
},
390+
},
391+
"succeeds with custom json codec": {
392+
config: func(string) *Config {
393+
return &Config{
394+
Host: "http://localhost",
395+
Token: "foo",
396+
JSONCodec: customJSONCodec{},
397+
}
398+
},
399+
expectedClient: &Client{
400+
host: "http://localhost/",
401+
token: "foo",
402+
client: &http.Client{
403+
Timeout: 600 * time.Second,
404+
},
405+
userAgent: defaultUserAgent(),
406+
jsonCodec: customJSONCodec{},
367407
},
368408
},
369409
"fails with missing address using config": {

0 commit comments

Comments
 (0)