Skip to content

Commit

Permalink
feat: allow configuring NewDecoder/NewEncoder via callbacks
Browse files Browse the repository at this point in the history
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))
```
  • Loading branch information
lithammer committed Sep 26, 2024
1 parent cd59f2f commit ef9413b
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 4 deletions.
55 changes: 53 additions & 2 deletions decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,60 @@ const (
defaultMaxSize = 16000
)

type option[T Decoder | Encoder] func(d *T)

// NewDecoder returns a new Decoder.
func NewDecoder() *Decoder {
return &Decoder{cache: newCache(), maxSize: defaultMaxSize}
func NewDecoder(opts ...option[Decoder]) *Decoder {
d := &Decoder{cache: newCache(), maxSize: defaultMaxSize}
for _, opt := range opts {
opt(d)
}
return d
}

// WithAliasTagDecoderOpt changes the tag used to locate custom field aliases.
// The default tag is "schema".
func WithAliasTagDecoderOpt(tag string) option[Decoder] {
return func(d *Decoder) {
d.SetAliasTag(tag)
}
}

// WithZeroEmptyDecoderOpt controls the behaviour when the decoder encounters empty values.
// in a map.
// If z is true and a key in the map has the empty string as a value
// then the corresponding struct field is set to the zero value.
// If z is false then empty strings are ignored.
//
// The default value is false, that is empty values do not change
// the value of the struct field.
func WithZeroEmptyDecoderOpt(z bool) option[Decoder] {
return func(d *Decoder) {
d.ZeroEmpty(z)
}
}

// WithIgnoreUnknownKeysDecoderOpt controls the behaviour when the decoder
// encounters unknown keys in the map.
// If i is true and an unknown field is encountered, it is ignored. This is
// similar to how unknown keys are handled by encoding/json.
// If i is false then Decode will return an error. Note that any valid keys
// will still be decoded in to the target struct.
//
// To preserve backwards compatibility, the default value is false.
func WithIgnoreUnknownKeysDecoderOpt(i bool) option[Decoder] {
return func(d *Decoder) {
d.IgnoreUnknownKeys(i)
}
}

// WithMaxSizeDecoderOpt limits the size of slices for URL nested arrays or object arrays.
// Choose MaxSize carefully; large values may create many zero-value slice elements.
// Example: "items.100000=apple" would create a slice with 100,000 empty strings.
func WithMaxSizeDecoderOpt(size int) option[Decoder] {
return func(d *Decoder) {
d.MaxSize(size)
}
}

// Decoder decodes values from a map[string][]string to a struct.
Expand Down
44 changes: 44 additions & 0 deletions decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2527,3 +2527,47 @@ func TestDecoder_SetMaxSize(t *testing.T) {
}
})
}

func TestNewDecoderWithOptions(t *testing.T) {
defaultDecoder := NewDecoder()

aliasTag := defaultDecoder.cache.tag + "-test"
decoder := NewDecoder(
WithAliasTagDecoderOpt(aliasTag),
WithZeroEmptyDecoderOpt(!defaultDecoder.zeroEmpty),
WithIgnoreUnknownKeysDecoderOpt(!defaultDecoder.ignoreUnknownKeys),
WithMaxSizeDecoderOpt(defaultDecoder.maxSize+1),
)

if decoder.cache.tag != aliasTag {
t.Errorf(
"Expected Decoder.cache.tag to be %q, got %q",
aliasTag,
decoder.cache.tag,
)
}

if decoder.ignoreUnknownKeys == defaultDecoder.ignoreUnknownKeys {
t.Errorf(
"Expected Decoder.ignoreUnknownKeys to be %t, got %t",
!decoder.ignoreUnknownKeys,
decoder.ignoreUnknownKeys,
)
}

if decoder.zeroEmpty == defaultDecoder.zeroEmpty {
t.Errorf(
"Expected Decoder.zeroEmpty to be %t, got %t",
!decoder.zeroEmpty,
decoder.zeroEmpty,
)
}

if decoder.maxSize != defaultDecoder.maxSize+1 {
t.Errorf(
"Expected Decoder.maxSize to be %d, got %d",
defaultDecoder.maxSize+1,
decoder.maxSize,
)
}
}
16 changes: 14 additions & 2 deletions encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,20 @@ type Encoder struct {
}

// NewEncoder returns a new Encoder with defaults.
func NewEncoder() *Encoder {
return &Encoder{cache: newCache(), regenc: make(map[reflect.Type]encoderFunc)}
func NewEncoder(opts ...option[Encoder]) *Encoder {
e := &Encoder{cache: newCache(), regenc: make(map[reflect.Type]encoderFunc)}
for _, opt := range opts {
opt(e)
}
return e
}

// WithAliasTagEncoderOpt changes the tag used to locate custom field aliases.
// The default tag is "schema".
func WithAliasTagEncoderOpt(tag string) option[Encoder] {
return func(e *Encoder) {
e.SetAliasTag(tag)
}
}

// Encode encodes a struct into map[string][]string.
Expand Down
17 changes: 17 additions & 0 deletions encoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,3 +523,20 @@ func TestRegisterEncoderWithPtrType(t *testing.T) {
valExists(t, "DateStart", ss.DateStart.time.String(), vals)
valExists(t, "DateEnd", "", vals)
}

func TestNewEncoderWithOptions(t *testing.T) {
defaultEncoder := NewEncoder()

aliasTag := defaultEncoder.cache.tag + "-test"
encoder := NewEncoder(
WithAliasTagEncoderOpt(aliasTag),
)

if encoder.cache.tag != aliasTag {
t.Errorf(
"Expected Encoder.cache.tag to be %q, got %q",
aliasTag,
encoder.cache.tag,
)
}
}

0 comments on commit ef9413b

Please sign in to comment.