Skip to content

Commit ef9413b

Browse files
committed
feat: allow configuring NewDecoder/NewEncoder via callbacks
Since GH-59 was rejected on the basis that it wasn't useful enough to break the public API. I took a stab at an alternate solution that doesn't break the public API. While still allowing one to configure the encoder and decoder when defining it as a package global (as recommended by the README). ```go var decoder = NewDecoder(WithIgnoreUnknownKeysDecoderOpt(true)) ```
1 parent cd59f2f commit ef9413b

File tree

4 files changed

+128
-4
lines changed

4 files changed

+128
-4
lines changed

decoder.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,60 @@ const (
1616
defaultMaxSize = 16000
1717
)
1818

19+
type option[T Decoder | Encoder] func(d *T)
20+
1921
// NewDecoder returns a new Decoder.
20-
func NewDecoder() *Decoder {
21-
return &Decoder{cache: newCache(), maxSize: defaultMaxSize}
22+
func NewDecoder(opts ...option[Decoder]) *Decoder {
23+
d := &Decoder{cache: newCache(), maxSize: defaultMaxSize}
24+
for _, opt := range opts {
25+
opt(d)
26+
}
27+
return d
28+
}
29+
30+
// WithAliasTagDecoderOpt changes the tag used to locate custom field aliases.
31+
// The default tag is "schema".
32+
func WithAliasTagDecoderOpt(tag string) option[Decoder] {
33+
return func(d *Decoder) {
34+
d.SetAliasTag(tag)
35+
}
36+
}
37+
38+
// WithZeroEmptyDecoderOpt controls the behaviour when the decoder encounters empty values.
39+
// in a map.
40+
// If z is true and a key in the map has the empty string as a value
41+
// then the corresponding struct field is set to the zero value.
42+
// If z is false then empty strings are ignored.
43+
//
44+
// The default value is false, that is empty values do not change
45+
// the value of the struct field.
46+
func WithZeroEmptyDecoderOpt(z bool) option[Decoder] {
47+
return func(d *Decoder) {
48+
d.ZeroEmpty(z)
49+
}
50+
}
51+
52+
// WithIgnoreUnknownKeysDecoderOpt controls the behaviour when the decoder
53+
// encounters unknown keys in the map.
54+
// If i is true and an unknown field is encountered, it is ignored. This is
55+
// similar to how unknown keys are handled by encoding/json.
56+
// If i is false then Decode will return an error. Note that any valid keys
57+
// will still be decoded in to the target struct.
58+
//
59+
// To preserve backwards compatibility, the default value is false.
60+
func WithIgnoreUnknownKeysDecoderOpt(i bool) option[Decoder] {
61+
return func(d *Decoder) {
62+
d.IgnoreUnknownKeys(i)
63+
}
64+
}
65+
66+
// WithMaxSizeDecoderOpt limits the size of slices for URL nested arrays or object arrays.
67+
// Choose MaxSize carefully; large values may create many zero-value slice elements.
68+
// Example: "items.100000=apple" would create a slice with 100,000 empty strings.
69+
func WithMaxSizeDecoderOpt(size int) option[Decoder] {
70+
return func(d *Decoder) {
71+
d.MaxSize(size)
72+
}
2273
}
2374

2475
// Decoder decodes values from a map[string][]string to a struct.

decoder_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2527,3 +2527,47 @@ func TestDecoder_SetMaxSize(t *testing.T) {
25272527
}
25282528
})
25292529
}
2530+
2531+
func TestNewDecoderWithOptions(t *testing.T) {
2532+
defaultDecoder := NewDecoder()
2533+
2534+
aliasTag := defaultDecoder.cache.tag + "-test"
2535+
decoder := NewDecoder(
2536+
WithAliasTagDecoderOpt(aliasTag),
2537+
WithZeroEmptyDecoderOpt(!defaultDecoder.zeroEmpty),
2538+
WithIgnoreUnknownKeysDecoderOpt(!defaultDecoder.ignoreUnknownKeys),
2539+
WithMaxSizeDecoderOpt(defaultDecoder.maxSize+1),
2540+
)
2541+
2542+
if decoder.cache.tag != aliasTag {
2543+
t.Errorf(
2544+
"Expected Decoder.cache.tag to be %q, got %q",
2545+
aliasTag,
2546+
decoder.cache.tag,
2547+
)
2548+
}
2549+
2550+
if decoder.ignoreUnknownKeys == defaultDecoder.ignoreUnknownKeys {
2551+
t.Errorf(
2552+
"Expected Decoder.ignoreUnknownKeys to be %t, got %t",
2553+
!decoder.ignoreUnknownKeys,
2554+
decoder.ignoreUnknownKeys,
2555+
)
2556+
}
2557+
2558+
if decoder.zeroEmpty == defaultDecoder.zeroEmpty {
2559+
t.Errorf(
2560+
"Expected Decoder.zeroEmpty to be %t, got %t",
2561+
!decoder.zeroEmpty,
2562+
decoder.zeroEmpty,
2563+
)
2564+
}
2565+
2566+
if decoder.maxSize != defaultDecoder.maxSize+1 {
2567+
t.Errorf(
2568+
"Expected Decoder.maxSize to be %d, got %d",
2569+
defaultDecoder.maxSize+1,
2570+
decoder.maxSize,
2571+
)
2572+
}
2573+
}

encoder.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,20 @@ type Encoder struct {
1616
}
1717

1818
// NewEncoder returns a new Encoder with defaults.
19-
func NewEncoder() *Encoder {
20-
return &Encoder{cache: newCache(), regenc: make(map[reflect.Type]encoderFunc)}
19+
func NewEncoder(opts ...option[Encoder]) *Encoder {
20+
e := &Encoder{cache: newCache(), regenc: make(map[reflect.Type]encoderFunc)}
21+
for _, opt := range opts {
22+
opt(e)
23+
}
24+
return e
25+
}
26+
27+
// WithAliasTagEncoderOpt changes the tag used to locate custom field aliases.
28+
// The default tag is "schema".
29+
func WithAliasTagEncoderOpt(tag string) option[Encoder] {
30+
return func(e *Encoder) {
31+
e.SetAliasTag(tag)
32+
}
2133
}
2234

2335
// Encode encodes a struct into map[string][]string.

encoder_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,3 +523,20 @@ func TestRegisterEncoderWithPtrType(t *testing.T) {
523523
valExists(t, "DateStart", ss.DateStart.time.String(), vals)
524524
valExists(t, "DateEnd", "", vals)
525525
}
526+
527+
func TestNewEncoderWithOptions(t *testing.T) {
528+
defaultEncoder := NewEncoder()
529+
530+
aliasTag := defaultEncoder.cache.tag + "-test"
531+
encoder := NewEncoder(
532+
WithAliasTagEncoderOpt(aliasTag),
533+
)
534+
535+
if encoder.cache.tag != aliasTag {
536+
t.Errorf(
537+
"Expected Encoder.cache.tag to be %q, got %q",
538+
aliasTag,
539+
encoder.cache.tag,
540+
)
541+
}
542+
}

0 commit comments

Comments
 (0)