From ef9413b7c887fdc230103f22f636f7f3eb76ffb7 Mon Sep 17 00:00:00 2001 From: Peter Lithammer Date: Thu, 30 Jun 2022 16:51:38 +0200 Subject: [PATCH 1/3] 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)) ``` --- decoder.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++-- decoder_test.go | 44 +++++++++++++++++++++++++++++++++++++++ encoder.go | 16 ++++++++++++-- encoder_test.go | 17 +++++++++++++++ 4 files changed, 128 insertions(+), 4 deletions(-) diff --git a/decoder.go b/decoder.go index 54c88ec..5d15115 100644 --- a/decoder.go +++ b/decoder.go @@ -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. diff --git a/decoder_test.go b/decoder_test.go index d01569e..cf59749 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -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, + ) + } +} diff --git a/encoder.go b/encoder.go index 52f2c10..4df0151 100644 --- a/encoder.go +++ b/encoder.go @@ -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. diff --git a/encoder_test.go b/encoder_test.go index 092f0de..a41afe0 100644 --- a/encoder_test.go +++ b/encoder_test.go @@ -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, + ) + } +} From 4423cb910859685c7350e37676093726c4088ca7 Mon Sep 17 00:00:00 2001 From: Peter Lithammer Date: Wed, 2 Oct 2024 11:02:55 +0200 Subject: [PATCH 2/3] fix: make the default cache tag a constant --- cache.go | 4 +++- encoder_test.go | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cache.go b/cache.go index 065b8d6..9107e0a 100644 --- a/cache.go +++ b/cache.go @@ -12,6 +12,8 @@ import ( "sync" ) +const defaultCacheTag = "schema" + var errInvalidPath = errors.New("schema: invalid path") // newCache returns a new cache. @@ -19,7 +21,7 @@ func newCache() *cache { c := cache{ m: make(map[reflect.Type]*structInfo), regconv: make(map[reflect.Type]Converter), - tag: "schema", + tag: defaultCacheTag, } return &c } diff --git a/encoder_test.go b/encoder_test.go index a41afe0..654df35 100644 --- a/encoder_test.go +++ b/encoder_test.go @@ -525,9 +525,7 @@ func TestRegisterEncoderWithPtrType(t *testing.T) { } func TestNewEncoderWithOptions(t *testing.T) { - defaultEncoder := NewEncoder() - - aliasTag := defaultEncoder.cache.tag + "-test" + aliasTag := defaultCacheTag + "-test" encoder := NewEncoder( WithAliasTagEncoderOpt(aliasTag), ) From 550ea33a1f2ae1a221dd436172bb5e70aff3b286 Mon Sep 17 00:00:00 2001 From: Peter Lithammer Date: Wed, 2 Oct 2024 11:04:14 +0200 Subject: [PATCH 3/3] doc: use links instead of copying docstring --- decoder.go | 25 ++++--------------------- encoder.go | 3 +-- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/decoder.go b/decoder.go index 5d15115..44a30e0 100644 --- a/decoder.go +++ b/decoder.go @@ -27,45 +27,28 @@ func NewDecoder(opts ...option[Decoder]) *Decoder { return d } -// WithAliasTagDecoderOpt changes the tag used to locate custom field aliases. -// The default tag is "schema". +// See [Decoder.SetAliasTag] for more information. 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. +// See [Decoder.ZeroEmpty] for more information. 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. +// See [Decoder.IgnoreUnknownKeys] for more information. 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. +// See [Decoder.MaxSize] for more information. func WithMaxSizeDecoderOpt(size int) option[Decoder] { return func(d *Decoder) { d.MaxSize(size) diff --git a/encoder.go b/encoder.go index 4df0151..215f511 100644 --- a/encoder.go +++ b/encoder.go @@ -24,8 +24,7 @@ func NewEncoder(opts ...option[Encoder]) *Encoder { return e } -// WithAliasTagEncoderOpt changes the tag used to locate custom field aliases. -// The default tag is "schema". +// See [Encoder.SetAliasTag] for more information. func WithAliasTagEncoderOpt(tag string) option[Encoder] { return func(e *Encoder) { e.SetAliasTag(tag)