From a99d3e3c6bbd0e600f01014635f306c7c559bdd5 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Fri, 24 May 2024 11:34:03 -0400 Subject: [PATCH] WIP --- bson/decoder.go | 6 +- bson/decoder_example_test.go | 10 ++- bson/decoder_test.go | 26 ++++--- bson/encoder.go | 6 +- bson/encoder_example_test.go | 21 ++++-- bson/encoder_test.go | 16 ++-- bson/registry.go | 19 +++-- bson/registry_option.go | 50 ++++++++----- bson/struct_codec.go | 65 ++++++++-------- bson/struct_tag_parser.go | 133 ++++++++++++++++++++------------- bson/struct_tag_parser_test.go | 128 ++++++++++++++++++++----------- bson/truncation_test.go | 22 ++++-- mongo/change_stream.go | 5 +- mongo/cursor.go | 36 ++++++--- mongo/mongo.go | 29 ++++--- mongo/single_result.go | 5 +- 16 files changed, 359 insertions(+), 218 deletions(-) diff --git a/bson/decoder.go b/bson/decoder.go index dd37150f60..7b0d0d68bd 100644 --- a/bson/decoder.go +++ b/bson/decoder.go @@ -18,7 +18,7 @@ var ErrDecodeToNil = errors.New("cannot Decode to nil value") // ConfigurableDecoderRegistry refers a DecoderRegistry that is configurable with *RegistryOpt. type ConfigurableDecoderRegistry interface { DecoderRegistry - SetCodecOptions(opts ...*RegistryOpt) + SetCodecOption(opt *RegistryOpt) error } // A Decoder reads and decodes BSON documents from a stream. It reads from a ValueReader as @@ -82,6 +82,6 @@ func (d *Decoder) Decode(val interface{}) error { } // SetBehavior set the decoder behavior with *RegistryOpt. -func (d *Decoder) SetBehavior(opts ...*RegistryOpt) { - d.reg.SetCodecOptions(opts...) +func (d *Decoder) SetBehavior(opt *RegistryOpt) error { + return d.reg.SetCodecOption(opt) } diff --git a/bson/decoder_example_test.go b/bson/decoder_example_test.go index 60fb360710..590756090d 100644 --- a/bson/decoder_example_test.go +++ b/bson/decoder_example_test.go @@ -77,7 +77,10 @@ func ExampleDecoder_SetBehavior_defaultDocumentM() { // type if the decode destination has no type information. The Properties // field in the City struct will be decoded as a "M" (i.e. map) instead // of the default "D". - decoder.SetBehavior(bson.DefaultDocumentM) + err = decoder.SetBehavior(bson.DefaultDocumentM) + if err != nil { + panic(err) + } var res City err = decoder.Decode(&res) @@ -114,7 +117,10 @@ func ExampleDecoder_SetBehavior_useJSONStructTags() { // Configure the Decoder to use "json" struct tags when decoding if "bson" // struct tags are not present. - decoder.SetBehavior(bson.UseJSONStructTags) + err = decoder.SetBehavior(bson.UseJSONStructTags) + if err != nil { + panic(err) + } var res Product err = decoder.Decode(&res) diff --git a/bson/decoder_test.go b/bson/decoder_test.go index 1c884c6d56..6ff2ad7545 100644 --- a/bson/decoder_test.go +++ b/bson/decoder_test.go @@ -253,7 +253,7 @@ func TestDecoderConfiguration(t *testing.T) { { description: "AllowTruncatingDoubles", configure: func(dec *Decoder) { - dec.SetBehavior(AllowTruncatingDoubles) + _ = dec.SetBehavior(AllowTruncatingDoubles) }, input: bsoncore.NewDocumentBuilder(). AppendDouble("myInt", 1.999). @@ -286,7 +286,7 @@ func TestDecoderConfiguration(t *testing.T) { { description: "BinaryAsSlice", configure: func(dec *Decoder) { - dec.SetBehavior(BinaryAsSlice) + _ = dec.SetBehavior(BinaryAsSlice) }, input: bsoncore.NewDocumentBuilder(). AppendBinary("myBinary", TypeBinaryGeneric, []byte{}). @@ -299,7 +299,7 @@ func TestDecoderConfiguration(t *testing.T) { { description: "DefaultDocumentD nested", configure: func(dec *Decoder) { - dec.SetBehavior(DefaultDocumentD) + _ = dec.SetBehavior(DefaultDocumentD) }, input: bsoncore.NewDocumentBuilder(). AppendDocument("myDocument", bsoncore.NewDocumentBuilder(). @@ -316,7 +316,7 @@ func TestDecoderConfiguration(t *testing.T) { { description: "DefaultDocumentM nested", configure: func(dec *Decoder) { - dec.SetBehavior(DefaultDocumentM) + _ = dec.SetBehavior(DefaultDocumentM) }, input: bsoncore.NewDocumentBuilder(). AppendDocument("myDocument", bsoncore.NewDocumentBuilder(). @@ -333,7 +333,7 @@ func TestDecoderConfiguration(t *testing.T) { { description: "UseJSONStructTags", configure: func(dec *Decoder) { - dec.SetBehavior(UseJSONStructTags) + _ = dec.SetBehavior(UseJSONStructTags) }, input: bsoncore.NewDocumentBuilder(). AppendString("jsonFieldName", "test value"). @@ -346,7 +346,7 @@ func TestDecoderConfiguration(t *testing.T) { { description: "UseLocalTimeZone", configure: func(dec *Decoder) { - dec.SetBehavior(UseLocalTimeZone) + _ = dec.SetBehavior(UseLocalTimeZone) }, input: bsoncore.NewDocumentBuilder(). AppendDateTime("myTime", 1684349179939). @@ -359,7 +359,7 @@ func TestDecoderConfiguration(t *testing.T) { { description: "ZeroMaps", configure: func(dec *Decoder) { - dec.SetBehavior(ZeroMaps) + _ = dec.SetBehavior(ZeroMaps) }, input: bsoncore.NewDocumentBuilder(). AppendDocument("myMap", bsoncore.NewDocumentBuilder(). @@ -376,7 +376,7 @@ func TestDecoderConfiguration(t *testing.T) { { description: "ZeroStructs", configure: func(dec *Decoder) { - dec.SetBehavior(ZeroStructs) + _ = dec.SetBehavior(ZeroStructs) }, input: bsoncore.NewDocumentBuilder(). AppendString("myString", "test value"). @@ -417,10 +417,11 @@ func TestDecoderConfiguration(t *testing.T) { dec := NewDecoder(NewValueReader(input)) - dec.SetBehavior(DefaultDocumentM) + err := dec.SetBehavior(DefaultDocumentM) + require.NoError(t, err, "SetBehavior error") var got interface{} - err := dec.Decode(&got) + err = dec.Decode(&got) require.NoError(t, err, "Decode error") want := M{ @@ -441,10 +442,11 @@ func TestDecoderConfiguration(t *testing.T) { dec := NewDecoder(NewValueReader(input)) - dec.SetBehavior(DefaultDocumentD) + err := dec.SetBehavior(DefaultDocumentD) + require.NoError(t, err, "SetBehavior error") var got interface{} - err := dec.Decode(&got) + err = dec.Decode(&got) require.NoError(t, err, "Decode error") want := D{ diff --git a/bson/encoder.go b/bson/encoder.go index 53db25e5c1..1317bee79e 100644 --- a/bson/encoder.go +++ b/bson/encoder.go @@ -13,7 +13,7 @@ import ( // ConfigurableEncoderRegistry refers a EncoderRegistry that is configurable with *RegistryOpt. type ConfigurableEncoderRegistry interface { EncoderRegistry - SetCodecOptions(opts ...*RegistryOpt) + SetCodecOption(opt *RegistryOpt) error } // An Encoder writes a serialization format to an output stream. It writes to a ValueWriter @@ -61,6 +61,6 @@ func (e *Encoder) Encode(val interface{}) error { } // SetBehavior set the encoder behavior with *RegistryOpt. -func (e *Encoder) SetBehavior(opts ...*RegistryOpt) { - e.reg.SetCodecOptions(opts...) +func (e *Encoder) SetBehavior(opt *RegistryOpt) error { + return e.reg.SetCodecOption(opt) } diff --git a/bson/encoder_example_test.go b/bson/encoder_example_test.go index f56249b7ef..60e85fabd3 100644 --- a/bson/encoder_example_test.go +++ b/bson/encoder_example_test.go @@ -64,9 +64,12 @@ func ExampleEncoder_SetBehavior_intMinSize() { vw := bson.NewValueWriter(buf) enc := bson.NewEncoder(vw) - enc.SetBehavior(bson.IntMinSize) + err := enc.SetBehavior(bson.IntMinSize) + if err != nil { + panic(err) + } - err := enc.Encode(foo{2}) + err = enc.Encode(foo{2}) if err != nil { panic(err) } @@ -84,14 +87,17 @@ func ExampleEncoder_SetBehavior_stringifyMapKeysWithFmt() { // Configure the Encoder to convert Go map keys to BSON document field names // using fmt.Sprintf instead of the default string conversion logic. - encoder.SetBehavior(bson.StringifyMapKeysWithFmt) + err := encoder.SetBehavior(bson.StringifyMapKeysWithFmt) + if err != nil { + panic(err) + } // Use the Encoder to marshal a BSON document that contains is a map of // city and state to a list of zip codes in that city. zipCodes := map[CityState][]int{ {City: "New York", State: "NY"}: {10001, 10301, 10451}, } - err := encoder.Encode(zipCodes) + err = encoder.Encode(zipCodes) if err != nil { panic(err) } @@ -115,7 +121,10 @@ func ExampleEncoder_SetBehavior_useJSONStructTags() { // Configure the Encoder to use "json" struct tags when decoding if "bson" // struct tags are not present. - encoder.SetBehavior(bson.UseJSONStructTags) + err := encoder.SetBehavior(bson.UseJSONStructTags) + if err != nil { + panic(err) + } // Use the Encoder to marshal a BSON document that contains the name, SKU, // and price (in cents) of a product. @@ -124,7 +133,7 @@ func ExampleEncoder_SetBehavior_useJSONStructTags() { SKU: "AB12345", Price: 399, } - err := encoder.Encode(product) + err = encoder.Encode(product) if err != nil { panic(err) } diff --git a/bson/encoder_test.go b/bson/encoder_test.go index 1110fc7d8d..a9f7376b5a 100644 --- a/bson/encoder_test.go +++ b/bson/encoder_test.go @@ -160,7 +160,7 @@ func TestEncoderConfiguration(t *testing.T) { { description: "ErrorOnInlineDuplicates", configure: func(enc *Encoder) { - enc.SetBehavior(ErrorOnInlineDuplicates) + _ = enc.SetBehavior(ErrorOnInlineDuplicates) }, input: inlineDuplicateOuter{ Inline: inlineDuplicateInner{Duplicate: "inner"}, @@ -173,7 +173,7 @@ func TestEncoderConfiguration(t *testing.T) { { description: "IntMinSize", configure: func(enc *Encoder) { - enc.SetBehavior(IntMinSize) + _ = enc.SetBehavior(IntMinSize) }, input: D{ {Key: "myInt", Value: int(1)}, @@ -194,7 +194,7 @@ func TestEncoderConfiguration(t *testing.T) { { description: "StringifyMapKeysWithFmt", configure: func(enc *Encoder) { - enc.SetBehavior(StringifyMapKeysWithFmt) + _ = enc.SetBehavior(StringifyMapKeysWithFmt) }, input: map[stringerTest]string{ {}: "test value", @@ -207,7 +207,7 @@ func TestEncoderConfiguration(t *testing.T) { { description: "NilMapAsEmpty", configure: func(enc *Encoder) { - enc.SetBehavior(NilMapAsEmpty) + _ = enc.SetBehavior(NilMapAsEmpty) }, input: D{{Key: "myMap", Value: map[string]string(nil)}}, want: bsoncore.NewDocumentBuilder(). @@ -218,7 +218,7 @@ func TestEncoderConfiguration(t *testing.T) { { description: "NilSliceAsEmpty", configure: func(enc *Encoder) { - enc.SetBehavior(NilSliceAsEmpty) + _ = enc.SetBehavior(NilSliceAsEmpty) }, input: D{{Key: "mySlice", Value: []string(nil)}}, want: bsoncore.NewDocumentBuilder(). @@ -229,7 +229,7 @@ func TestEncoderConfiguration(t *testing.T) { { description: "NilByteSliceAsEmpty", configure: func(enc *Encoder) { - enc.SetBehavior(NilByteSliceAsEmpty) + _ = enc.SetBehavior(NilByteSliceAsEmpty) }, input: D{{Key: "myBytes", Value: []byte(nil)}}, want: bsoncore.NewDocumentBuilder(). @@ -241,7 +241,7 @@ func TestEncoderConfiguration(t *testing.T) { { description: "OmitZeroStruct", configure: func(enc *Encoder) { - enc.SetBehavior(OmitZeroStruct) + _ = enc.SetBehavior(OmitZeroStruct) }, input: struct { Zero zeroStruct `bson:",omitempty"` @@ -253,7 +253,7 @@ func TestEncoderConfiguration(t *testing.T) { { description: "UseJSONStructTags", configure: func(enc *Encoder) { - enc.SetBehavior(UseJSONStructTags) + _ = enc.SetBehavior(UseJSONStructTags) }, input: struct { StructFieldName string `json:"jsonFieldName"` diff --git a/bson/registry.go b/bson/registry.go index 6deb66c6a3..cf88c703ba 100644 --- a/bson/registry.go +++ b/bson/registry.go @@ -333,16 +333,21 @@ type Registry struct { codecTypeMap map[reflect.Type][]interface{} } -// SetCodecOptions configures Registry using a *RegistryOpt. -func (r *Registry) SetCodecOptions(opts ...*RegistryOpt) { - for _, opt := range opts { - v, ok := r.codecTypeMap[opt.typ] - if ok && v != nil { - for i := range v { - _ = opt.fn.Call([]reflect.Value{reflect.ValueOf(v[i])}) +// SetCodecOption configures Registry using a *RegistryOpt. +func (r *Registry) SetCodecOption(opt *RegistryOpt) error { + v, ok := r.codecTypeMap[opt.typ] + if !ok || len(v) == 0 { + return fmt.Errorf("could not find codec %s", opt.typ.String()) + } + for i := range v { + rtns := opt.fn.Call([]reflect.Value{reflect.ValueOf(v[i])}) + for _, r := range rtns { + if !r.IsNil() { + return r.Interface().(error) } } } + return nil } // LookupEncoder returns the first matching encoder in the Registry. It uses the following lookup diff --git a/bson/registry_option.go b/bson/registry_option.go index b655b1af47..5f07052f5c 100644 --- a/bson/registry_option.go +++ b/bson/registry_option.go @@ -27,7 +27,7 @@ type RegistryOpt struct { // reg.SetCodecOptions(opt) // // The "attr" field in the registered Codec can be set to "value". -func NewRegistryOpt[T any](fn func(T)) *RegistryOpt { +func NewRegistryOpt[T any](fn func(T) error) *RegistryOpt { var zero [0]T return &RegistryOpt{ typ: reflect.TypeOf(zero).Elem(), @@ -37,75 +37,87 @@ func NewRegistryOpt[T any](fn func(T)) *RegistryOpt { // NilByteSliceAsEmpty causes the Encoder to marshal nil Go byte slices as empty BSON binary values // instead of BSON null. -var NilByteSliceAsEmpty = NewRegistryOpt(func(c *byteSliceCodec) { +var NilByteSliceAsEmpty = NewRegistryOpt(func(c *byteSliceCodec) error { c.encodeNilAsEmpty = true + return nil }) // BinaryAsSlice causes the Decoder to unmarshal BSON binary field values that are the "Generic" or // "Old" BSON binary subtype as a Go byte slice instead of a primitive.Binary. -var BinaryAsSlice = NewRegistryOpt(func(c *emptyInterfaceCodec) { +var BinaryAsSlice = NewRegistryOpt(func(c *emptyInterfaceCodec) error { c.decodeBinaryAsSlice = true + return nil }) // DefaultDocumentM causes the Decoder to always unmarshal documents into the primitive.M type. This // behavior is restricted to data typed as "interface{}" or "map[string]interface{}". -var DefaultDocumentM = NewRegistryOpt(func(c *emptyInterfaceCodec) { +var DefaultDocumentM = NewRegistryOpt(func(c *emptyInterfaceCodec) error { c.defaultDocumentType = reflect.TypeOf(M{}) + return nil }) // DefaultDocumentD causes the Decoder to always unmarshal documents into the primitive.D type. This // behavior is restricted to data typed as "interface{}" or "map[string]interface{}". -var DefaultDocumentD = NewRegistryOpt(func(c *emptyInterfaceCodec) { +var DefaultDocumentD = NewRegistryOpt(func(c *emptyInterfaceCodec) error { c.defaultDocumentType = reflect.TypeOf(D{}) + return nil }) // NilMapAsEmpty causes the Encoder to marshal nil Go maps as empty BSON documents instead of BSON // null. -var NilMapAsEmpty = NewRegistryOpt(func(c *mapCodec) { +var NilMapAsEmpty = NewRegistryOpt(func(c *mapCodec) error { c.encodeNilAsEmpty = true + return nil }) // StringifyMapKeysWithFmt causes the Encoder to convert Go map keys to BSON document field name // strings using fmt.Sprint instead of the default string conversion logic. -var StringifyMapKeysWithFmt = NewRegistryOpt(func(c *mapCodec) { +var StringifyMapKeysWithFmt = NewRegistryOpt(func(c *mapCodec) error { c.encodeKeysWithStringer = true + return nil }) // ZeroMaps causes the Decoder to delete any existing values from Go maps in the destination value // passed to Decode before unmarshaling BSON documents into them. -var ZeroMaps = NewRegistryOpt(func(c *mapCodec) { +var ZeroMaps = NewRegistryOpt(func(c *mapCodec) error { c.decodeZerosMap = true + return nil }) // AllowTruncatingDoubles causes the Decoder to truncate the fractional part of BSON "double" values // when attempting to unmarshal them into a Go integer (int, int8, int16, int32, or int64) struct // field. The truncation logic does not apply to BSON "decimal128" values. -var AllowTruncatingDoubles = NewRegistryOpt(func(c *numCodec) { +var AllowTruncatingDoubles = NewRegistryOpt(func(c *numCodec) error { c.truncate = true + return nil }) // IntMinSize causes the Encoder to marshal Go integer values (int, int8, int16, int32, int64, uint, // uint8, uint16, uint32, or uint64) as the minimum BSON int size (either 32 or 64 bits) that can // represent the integer value. -var IntMinSize = NewRegistryOpt(func(c *numCodec) { +var IntMinSize = NewRegistryOpt(func(c *numCodec) error { c.minSize = true + return nil }) // NilSliceAsEmpty causes the Encoder to marshal nil Go slices as empty BSON arrays instead of BSON // null. -var NilSliceAsEmpty = NewRegistryOpt(func(c *sliceCodec) { +var NilSliceAsEmpty = NewRegistryOpt(func(c *sliceCodec) error { c.encodeNilAsEmpty = true + return nil }) // DecodeObjectIDAsHex causes the Decoder to unmarshal BSON ObjectID as a hexadecimal string. -var DecodeObjectIDAsHex = NewRegistryOpt(func(c *stringCodec) { +var DecodeObjectIDAsHex = NewRegistryOpt(func(c *stringCodec) error { c.decodeObjectIDAsHex = true + return nil }) // ErrorOnInlineDuplicates causes the Encoder to return an error if there is a duplicate field in // the marshaled BSON when the "inline" struct tag option is set. -var ErrorOnInlineDuplicates = NewRegistryOpt(func(c *structCodec) { +var ErrorOnInlineDuplicates = NewRegistryOpt(func(c *structCodec) error { c.overwriteDuplicatedInlinedFields = false + return nil }) // TODO(GODRIVER-2820): Update the description to remove the note about only examining exported @@ -116,24 +128,28 @@ var ErrorOnInlineDuplicates = NewRegistryOpt(func(c *structCodec) { // // Note that the Encoder only examines exported struct fields when determining if a struct is the // zero value. It considers pointers to a zero struct value (e.g. &MyStruct{}) not empty. -var OmitZeroStruct = NewRegistryOpt(func(c *structCodec) { +var OmitZeroStruct = NewRegistryOpt(func(c *structCodec) error { c.encodeOmitDefaultStruct = true + return nil }) // UseJSONStructTags causes the Encoder and Decoder to fall back to using the "json" struct tag if // a "bson" struct tag is not specified. -var UseJSONStructTags = NewRegistryOpt(func(c *structCodec) { +var UseJSONStructTags = NewRegistryOpt(func(c *structCodec) error { c.useJSONStructTags = true + return nil }) // ZeroStructs causes the Decoder to delete any existing values from Go structs in the destination // value passed to Decode before unmarshaling BSON documents into them. -var ZeroStructs = NewRegistryOpt(func(c *structCodec) { +var ZeroStructs = NewRegistryOpt(func(c *structCodec) error { c.decodeZeroStruct = true + return nil }) // UseLocalTimeZone causes the Decoder to unmarshal time.Time values in the local timezone instead // of the UTC timezone. -var UseLocalTimeZone = NewRegistryOpt(func(c *timeCodec) { +var UseLocalTimeZone = NewRegistryOpt(func(c *timeCodec) error { c.useLocalTimeZone = true + return nil }) diff --git a/bson/struct_codec.go b/bson/struct_codec.go index dc42b31150..24ea1a2018 100644 --- a/bson/struct_codec.go +++ b/bson/struct_codec.go @@ -50,7 +50,7 @@ func (de *DecodeError) Keys() []string { // structCodec is the Codec used for struct values. type structCodec struct { cache sync.Map // map[reflect.Type]*structDescription - parser StructTagParser + parser structTagParser // decodeZeroStruct causes DecodeValue to delete any existing values from Go structs in the decodeZeroStruct bool @@ -76,7 +76,7 @@ type structCodec struct { } // newStructCodec returns a StructCodec that uses p for struct tag parsing. -func newStructCodec(p StructTagParser) *structCodec { +func newStructCodec(p structTagParser) *structCodec { return &structCodec{ parser: p, overwriteDuplicatedInlinedFields: true, @@ -84,25 +84,12 @@ func newStructCodec(p StructTagParser) *structCodec { } type localEncoderRegistry struct { - registry EncoderRegistry - - minSize bool + registry EncoderRegistry + encoderLookup func(EncoderRegistry, reflect.Type) (ValueEncoder, error) } func (r *localEncoderRegistry) LookupEncoder(t reflect.Type) (ValueEncoder, error) { - ve, err := r.registry.LookupEncoder(t) - if err != nil { - return ve, err - } - if r.minSize { - if ic, ok := ve.(*numCodec); ok { - ve = &numCodec{ - minSize: true, - truncate: ic.truncate, - } - } - } - return ve, nil + return r.encoderLookup(r.registry, t) } // EncodeValue handles encoding generic struct types. @@ -132,8 +119,8 @@ func (sc *structCodec) EncodeValue(reg EncoderRegistry, vw ValueWriter, val refl } reg = &localEncoderRegistry{ - registry: reg, - minSize: desc.minSize, + registry: reg, + encoderLookup: desc.encoderLookup, } var encoder ValueEncoder @@ -395,14 +382,13 @@ type structDescription struct { } type fieldDescription struct { - name string // BSON key name - fieldName string // struct field name - idx int - omitEmpty bool - minSize bool - truncate bool - inline []int - fieldType reflect.Type + name string // BSON key name + fieldName string // struct field name + idx int + inline []int + omitEmpty bool + fieldType reflect.Type + encoderLookup func(EncoderRegistry, reflect.Type) (ValueEncoder, error) } type byIndex []fieldDescription @@ -484,14 +470,14 @@ func (sc *structCodec) describeStructSlow( fieldType: sfType, } - var stags StructTags + var stags *structTags var err error // If the caller requested that we use JSON struct tags, use the JSONFallbackStructTagParser // instead of the parser defined on the codec. if useJSONStructTags { - stags, err = JSONFallbackStructTagParser.ParseStructTags(sf) + stags, err = sc.parser.parseJSONStructTags(sf) } else { - stags, err = sc.parser.ParseStructTags(sf) + stags, err = sc.parser.parseStructTags(sf) } if err != nil { return nil, err @@ -501,8 +487,21 @@ func (sc *structCodec) describeStructSlow( } description.name = stags.Name description.omitEmpty = stags.OmitEmpty - description.minSize = stags.MinSize - description.truncate = stags.Truncate + description.encoderLookup = func(reg EncoderRegistry, t reflect.Type) (ValueEncoder, error) { + if stags.LookupEncoderOnMinSize != nil { + reg = &localEncoderRegistry{ + registry: reg, + encoderLookup: stags.LookupEncoderOnMinSize.LookupEncoder, + } + } + if stags.LookupEncoderOnTruncate != nil { + reg = &localEncoderRegistry{ + registry: reg, + encoderLookup: stags.LookupEncoderOnTruncate.LookupEncoder, + } + } + return reg.LookupEncoder(t) + } if stags.Inline { sd.inline = true diff --git a/bson/struct_tag_parser.go b/bson/struct_tag_parser.go index d116c14040..30b0e9815d 100644 --- a/bson/struct_tag_parser.go +++ b/bson/struct_tag_parser.go @@ -11,25 +11,57 @@ import ( "strings" ) -// StructTagParser returns the struct tags for a given struct field. -// -// Deprecated: Defining custom BSON struct tag parsers will not be supported in Go Driver 2.0. -type StructTagParser interface { - ParseStructTags(reflect.StructField) (StructTags, error) +// structTagParser returns the struct tags for a given reflect.StructField. +type structTagParser interface { + parseStructTags(reflect.StructField) (*structTags, error) + parseJSONStructTags(reflect.StructField) (*structTags, error) } -// StructTagParserFunc is an adapter that allows a generic function to be used -// as a StructTagParser. -// -// Deprecated: Defining custom BSON struct tag parsers will not be supported in Go Driver 2.0. -type StructTagParserFunc func(reflect.StructField) (StructTags, error) +// DefaultStructTagParser is the StructTagParser used by the StructCodec by default. +var DefaultStructTagParser = &StructTagParser{ + LookupEncoderOnMinSize: retrieverOnMinSize{}, + LookupEncoderOnTruncate: retrieverOnTruncate{}, +} + +type retrieverOnMinSize struct{} + +func (retrieverOnMinSize) LookupEncoder(reg EncoderRegistry, t reflect.Type) (ValueEncoder, error) { + enc, err := reg.LookupEncoder(t) + if err != nil { + return enc, err + } + switch t.Kind() { + case reflect.Int64, reflect.Uint, reflect.Uint32, reflect.Uint64: + if codec, ok := enc.(*numCodec); ok { + c := *codec + c.minSize = true + return &c, nil + } + } + return enc, nil +} + +type retrieverOnTruncate struct{} -// ParseStructTags implements the StructTagParser interface. -func (stpf StructTagParserFunc) ParseStructTags(sf reflect.StructField) (StructTags, error) { - return stpf(sf) +func (retrieverOnTruncate) LookupEncoder(reg EncoderRegistry, t reflect.Type) (ValueEncoder, error) { + enc, err := reg.LookupEncoder(t) + if err != nil { + return enc, err + } + switch t.Kind() { + case reflect.Float32, + reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int, + reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + if codec, ok := enc.(*numCodec); ok { + c := *codec + c.truncate = true + return &c, nil + } + } + return enc, nil } -// StructTags represents the struct tag fields that the StructCodec uses during +// structTags represents the struct tag fields that the StructCodec uses during // the encoding and decoding process. // // In the case of a struct, the lowercased field name is used as the key for each exported @@ -38,34 +70,37 @@ func (stpf StructTagParserFunc) ParseStructTags(sf reflect.StructField) (StructT // // The properties are defined below: // -// OmitEmpty Only include the field if it's not set to the zero value for the type or to -// empty slices or maps. -// -// MinSize Marshal an integer of a type larger than 32 bits value as an int32, if that's -// feasible while preserving the numeric value. -// -// Truncate When unmarshaling a BSON double, it is permitted to lose precision to fit within -// a float32. -// -// Inline Inline the field, which must be a struct or a map, causing all of its fields +// inline Inline the field, which must be a struct or a map, causing all of its fields // or keys to be processed as if they were part of the outer struct. For maps, // keys must not conflict with the bson keys of other struct fields. // -// Skip This struct field should be skipped. This is usually denoted by parsing a "-" -// for the name. +// omitEmpty Only include the field if it's not set to the zero value for the type or to +// empty slices or maps. // -// Deprecated: Defining custom BSON struct tag parsers will not be supported in Go Driver 2.0. -type StructTags struct { +// skip This struct field should be skipped. This is usually denoted by parsing a "-" +// for the name. +type structTags struct { Name string - OmitEmpty bool - MinSize bool - Truncate bool Inline bool + OmitEmpty bool Skip bool + + LookupEncoderOnMinSize EncoderRetriever + LookupEncoderOnTruncate EncoderRetriever } -// DefaultStructTagParser is the StructTagParser used by the StructCodec by default. -// It will handle the bson struct tag. See the documentation for StructTags to see +// EncoderRetriever is used to look up ValueEncoder with given EncoderRegistry and reflect.Type. +type EncoderRetriever interface { + LookupEncoder(EncoderRegistry, reflect.Type) (ValueEncoder, error) +} + +// StructTagParser defines the encoder lookup bahavior when minSize and truncate tags are set. +type StructTagParser struct { + LookupEncoderOnMinSize EncoderRetriever + LookupEncoderOnTruncate EncoderRetriever +} + +// parseStructTags handles the bson struct tag. See the documentation for StructTags to see // what each of the returned fields means. // // If there is no name in the struct tag fields, the struct field name is lowercased. @@ -89,22 +124,20 @@ type StructTags struct { // A struct tag either consisting entirely of '-' or with a bson key with a // value consisting entirely of '-' will return a StructTags with Skip true and // the remaining fields will be their default values. -// -// Deprecated: DefaultStructTagParser will be removed in Go Driver 2.0. -var DefaultStructTagParser StructTagParserFunc = func(sf reflect.StructField) (StructTags, error) { +func (p *StructTagParser) parseStructTags(sf reflect.StructField) (*structTags, error) { key := strings.ToLower(sf.Name) tag, ok := sf.Tag.Lookup("bson") if !ok && !strings.Contains(string(sf.Tag), ":") && len(sf.Tag) > 0 { tag = string(sf.Tag) } - return parseTags(key, tag) + return p.parseTags(key, tag) } -func parseTags(key string, tag string) (StructTags, error) { - var st StructTags +func (p *StructTagParser) parseTags(key string, tag string) (*structTags, error) { + var st structTags if tag == "-" { st.Skip = true - return st, nil + return &st, nil } for idx, str := range strings.Split(tag, ",") { @@ -112,29 +145,25 @@ func parseTags(key string, tag string) (StructTags, error) { key = str } switch str { + case "inline": + st.Inline = true case "omitempty": st.OmitEmpty = true case "minsize": - st.MinSize = true + st.LookupEncoderOnMinSize = p.LookupEncoderOnMinSize case "truncate": - st.Truncate = true - case "inline": - st.Inline = true + st.LookupEncoderOnTruncate = p.LookupEncoderOnTruncate } } st.Name = key - return st, nil + return &st, nil } -// JSONFallbackStructTagParser has the same behavior as DefaultStructTagParser -// but will also fallback to parsing the json tag instead on a field where the +// parseJSONStructTags parses the json tag instead on a field where the // bson tag isn't available. -// -// Deprecated: Use [go.mongodb.org/mongo-driver/bson.Encoder.UseJSONStructTags] and -// [go.mongodb.org/mongo-driver/bson.Decoder.UseJSONStructTags] instead. -var JSONFallbackStructTagParser StructTagParserFunc = func(sf reflect.StructField) (StructTags, error) { +func (p *StructTagParser) parseJSONStructTags(sf reflect.StructField) (*structTags, error) { key := strings.ToLower(sf.Name) tag, ok := sf.Tag.Lookup("bson") if !ok { @@ -144,5 +173,5 @@ var JSONFallbackStructTagParser StructTagParserFunc = func(sf reflect.StructFiel tag = string(sf.Tag) } - return parseTags(key, tag) + return p.parseTags(key, tag) } diff --git a/bson/struct_tag_parser_test.go b/bson/struct_tag_parser_test.go index b03815488a..312d1ab7f3 100644 --- a/bson/struct_tag_parser_test.go +++ b/bson/struct_tag_parser_test.go @@ -17,134 +17,174 @@ func TestStructTagParsers(t *testing.T) { testCases := []struct { name string sf reflect.StructField - want StructTags - parser StructTagParserFunc + want *structTags + parser func(reflect.StructField) (*structTags, error) }{ { "default no bson tag", reflect.StructField{Name: "foo", Tag: reflect.StructTag("bar")}, - StructTags{Name: "bar"}, - DefaultStructTagParser, + &structTags{Name: "bar"}, + DefaultStructTagParser.parseStructTags, }, { "default empty", reflect.StructField{Name: "foo", Tag: reflect.StructTag("")}, - StructTags{Name: "foo"}, - DefaultStructTagParser, + &structTags{Name: "foo"}, + DefaultStructTagParser.parseStructTags, }, { "default tag only dash", reflect.StructField{Name: "foo", Tag: reflect.StructTag("-")}, - StructTags{Skip: true}, - DefaultStructTagParser, + &structTags{Skip: true}, + DefaultStructTagParser.parseStructTags, }, { "default bson tag only dash", reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:"-"`)}, - StructTags{Skip: true}, - DefaultStructTagParser, + &structTags{Skip: true}, + DefaultStructTagParser.parseStructTags, }, { "default all options", reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bar,omitempty,minsize,truncate,inline`)}, - StructTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, - DefaultStructTagParser, + &structTags{ + Name: "bar", Inline: true, OmitEmpty: true, + LookupEncoderOnMinSize: retrieverOnMinSize{}, + LookupEncoderOnTruncate: retrieverOnTruncate{}, + }, + DefaultStructTagParser.parseStructTags, }, { "default all options default name", reflect.StructField{Name: "foo", Tag: reflect.StructTag(`,omitempty,minsize,truncate,inline`)}, - StructTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, - DefaultStructTagParser, + &structTags{ + Name: "foo", Inline: true, OmitEmpty: true, + LookupEncoderOnMinSize: retrieverOnMinSize{}, + LookupEncoderOnTruncate: retrieverOnTruncate{}, + }, + DefaultStructTagParser.parseStructTags, }, { "default bson tag all options", reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:"bar,omitempty,minsize,truncate,inline"`)}, - StructTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, - DefaultStructTagParser, + &structTags{ + Name: "bar", Inline: true, OmitEmpty: true, + LookupEncoderOnMinSize: retrieverOnMinSize{}, + LookupEncoderOnTruncate: retrieverOnTruncate{}, + }, + DefaultStructTagParser.parseStructTags, }, { "default bson tag all options default name", reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:",omitempty,minsize,truncate,inline"`)}, - StructTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, - DefaultStructTagParser, + &structTags{ + Name: "foo", Inline: true, OmitEmpty: true, + LookupEncoderOnMinSize: retrieverOnMinSize{}, + LookupEncoderOnTruncate: retrieverOnTruncate{}, + }, + DefaultStructTagParser.parseStructTags, }, { "default ignore xml", reflect.StructField{Name: "foo", Tag: reflect.StructTag(`xml:"bar"`)}, - StructTags{Name: "foo"}, - DefaultStructTagParser, + &structTags{Name: "foo"}, + DefaultStructTagParser.parseStructTags, }, { "JSONFallback no bson tag", reflect.StructField{Name: "foo", Tag: reflect.StructTag("bar")}, - StructTags{Name: "bar"}, - JSONFallbackStructTagParser, + &structTags{Name: "bar"}, + DefaultStructTagParser.parseJSONStructTags, }, { "JSONFallback empty", reflect.StructField{Name: "foo", Tag: reflect.StructTag("")}, - StructTags{Name: "foo"}, - JSONFallbackStructTagParser, + &structTags{Name: "foo"}, + DefaultStructTagParser.parseJSONStructTags, }, { "JSONFallback tag only dash", reflect.StructField{Name: "foo", Tag: reflect.StructTag("-")}, - StructTags{Skip: true}, - JSONFallbackStructTagParser, + &structTags{Skip: true}, + DefaultStructTagParser.parseJSONStructTags, }, { "JSONFallback bson tag only dash", reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:"-"`)}, - StructTags{Skip: true}, - JSONFallbackStructTagParser, + &structTags{Skip: true}, + DefaultStructTagParser.parseJSONStructTags, }, { "JSONFallback all options", reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bar,omitempty,minsize,truncate,inline`)}, - StructTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, - JSONFallbackStructTagParser, + &structTags{ + Name: "bar", Inline: true, OmitEmpty: true, + LookupEncoderOnMinSize: retrieverOnMinSize{}, + LookupEncoderOnTruncate: retrieverOnTruncate{}, + }, + DefaultStructTagParser.parseJSONStructTags, }, { "JSONFallback all options default name", reflect.StructField{Name: "foo", Tag: reflect.StructTag(`,omitempty,minsize,truncate,inline`)}, - StructTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, - JSONFallbackStructTagParser, + &structTags{ + Name: "foo", Inline: true, OmitEmpty: true, + LookupEncoderOnMinSize: retrieverOnMinSize{}, + LookupEncoderOnTruncate: retrieverOnTruncate{}, + }, + DefaultStructTagParser.parseJSONStructTags, }, { "JSONFallback bson tag all options", reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:"bar,omitempty,minsize,truncate,inline"`)}, - StructTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, - JSONFallbackStructTagParser, + &structTags{ + Name: "bar", Inline: true, OmitEmpty: true, + LookupEncoderOnMinSize: retrieverOnMinSize{}, + LookupEncoderOnTruncate: retrieverOnTruncate{}, + }, + DefaultStructTagParser.parseJSONStructTags, }, { "JSONFallback bson tag all options default name", reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:",omitempty,minsize,truncate,inline"`)}, - StructTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, - JSONFallbackStructTagParser, + &structTags{ + Name: "foo", Inline: true, OmitEmpty: true, + LookupEncoderOnMinSize: retrieverOnMinSize{}, + LookupEncoderOnTruncate: retrieverOnTruncate{}, + }, + DefaultStructTagParser.parseJSONStructTags, }, { "JSONFallback json tag all options", reflect.StructField{Name: "foo", Tag: reflect.StructTag(`json:"bar,omitempty,minsize,truncate,inline"`)}, - StructTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, - JSONFallbackStructTagParser, + &structTags{ + Name: "bar", Inline: true, OmitEmpty: true, + LookupEncoderOnMinSize: retrieverOnMinSize{}, + LookupEncoderOnTruncate: retrieverOnTruncate{}, + }, + DefaultStructTagParser.parseJSONStructTags, }, { "JSONFallback json tag all options default name", reflect.StructField{Name: "foo", Tag: reflect.StructTag(`json:",omitempty,minsize,truncate,inline"`)}, - StructTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, - JSONFallbackStructTagParser, + &structTags{ + Name: "foo", Inline: true, OmitEmpty: true, + LookupEncoderOnMinSize: retrieverOnMinSize{}, + LookupEncoderOnTruncate: retrieverOnTruncate{}, + }, + DefaultStructTagParser.parseJSONStructTags, }, { "JSONFallback bson tag overrides other tags", reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:"bar" json:"qux,truncate"`)}, - StructTags{Name: "bar"}, - JSONFallbackStructTagParser, + &structTags{Name: "bar"}, + DefaultStructTagParser.parseJSONStructTags, }, { "JSONFallback ignore xml", reflect.StructField{Name: "foo", Tag: reflect.StructTag(`xml:"bar"`)}, - StructTags{Name: "foo"}, - JSONFallbackStructTagParser, + &structTags{Name: "foo"}, + DefaultStructTagParser.parseJSONStructTags, }, } diff --git a/bson/truncation_test.go b/bson/truncation_test.go index 311b2942d4..e0a1579494 100644 --- a/bson/truncation_test.go +++ b/bson/truncation_test.go @@ -33,16 +33,19 @@ func TestTruncation(t *testing.T) { buf := new(bytes.Buffer) vw := NewValueWriter(buf) enc := NewEncoderWithRegistry(NewRegistryBuilder().Build(), vw) - enc.SetBehavior(IntMinSize) - err := enc.Encode(&input) + err := enc.SetBehavior(IntMinSize) + assert.Nil(t, err) + err = enc.Encode(&input) assert.Nil(t, err) var output outputArgs - opt := NewRegistryOpt(func(c *numCodec) { + opt := NewRegistryOpt(func(c *numCodec) error { c.truncate = true + return nil }) reg := NewRegistryBuilder().Build() - reg.SetCodecOptions(opt) + err = reg.SetCodecOption(opt) + assert.Nil(t, err) err = UnmarshalWithContext(reg, buf.Bytes(), &output) assert.Nil(t, err) @@ -59,16 +62,19 @@ func TestTruncation(t *testing.T) { buf := new(bytes.Buffer) vw := NewValueWriter(buf) enc := NewEncoderWithRegistry(NewRegistryBuilder().Build(), vw) - enc.SetBehavior(IntMinSize) - err := enc.Encode(&input) + err := enc.SetBehavior(IntMinSize) + assert.Nil(t, err) + err = enc.Encode(&input) assert.Nil(t, err) var output outputArgs - opt := NewRegistryOpt(func(c *numCodec) { + opt := NewRegistryOpt(func(c *numCodec) error { c.truncate = false + return nil }) reg := NewRegistryBuilder().Build() - reg.SetCodecOptions(opt) + err = reg.SetCodecOption(opt) + assert.Nil(t, err) // case throws an error when truncation is disabled err = UnmarshalWithContext(reg, buf.Bytes(), &output) diff --git a/mongo/change_stream.go b/mongo/change_stream.go index d578abce63..a63dcd8bb7 100644 --- a/mongo/change_stream.go +++ b/mongo/change_stream.go @@ -596,7 +596,10 @@ func (cs *ChangeStream) Decode(val interface{}) error { return ErrNilCursor } - dec := getDecoder(cs.Current, cs.bsonOpts, cs.registry) + dec, err := getDecoder(cs.Current, cs.bsonOpts, cs.registry) + if err != nil { + return err + } return dec.Decode(val) } diff --git a/mongo/cursor.go b/mongo/cursor.go index 703aebf90b..bf5fc789bd 100644 --- a/mongo/cursor.go +++ b/mongo/cursor.go @@ -234,7 +234,7 @@ func getDecoder( data []byte, opts *options.BSONOptions, reg *bson.Registry, -) *bson.Decoder { +) (*bson.Decoder, error) { vr := bson.NewValueReader(data) var dec *bson.Decoder if reg != nil { @@ -244,39 +244,47 @@ func getDecoder( } if opts != nil { + regOpts := []*bson.RegistryOpt{} if opts.AllowTruncatingDoubles { - dec.SetBehavior(bson.AllowTruncatingDoubles) + regOpts = append(regOpts, bson.AllowTruncatingDoubles) } if opts.BinaryAsSlice { - dec.SetBehavior(bson.BinaryAsSlice) + regOpts = append(regOpts, bson.BinaryAsSlice) } if opts.DefaultDocumentD { - dec.SetBehavior(bson.DefaultDocumentD) + regOpts = append(regOpts, bson.DefaultDocumentD) } if opts.DefaultDocumentM { - dec.SetBehavior(bson.DefaultDocumentM) + regOpts = append(regOpts, bson.DefaultDocumentM) } if opts.UseJSONStructTags { - dec.SetBehavior(bson.UseJSONStructTags) + regOpts = append(regOpts, bson.UseJSONStructTags) } if opts.UseLocalTimeZone { - dec.SetBehavior(bson.UseLocalTimeZone) + regOpts = append(regOpts, bson.UseLocalTimeZone) } if opts.ZeroMaps { - dec.SetBehavior(bson.ZeroMaps) + regOpts = append(regOpts, bson.ZeroMaps) } if opts.ZeroStructs { - dec.SetBehavior(bson.ZeroStructs) + regOpts = append(regOpts, bson.ZeroStructs) + } + for _, opt := range regOpts { + err := dec.SetBehavior(opt) + return nil, err } } - return dec + return dec, nil } // Decode will unmarshal the current document into val and return any errors from the unmarshalling process without any // modification. If val is nil or is a typed nil, an error will be returned. func (c *Cursor) Decode(val interface{}) error { - dec := getDecoder(c.Current, c.bsonOpts, c.registry) + dec, err := getDecoder(c.Current, c.bsonOpts, c.registry) + if err != nil { + return err + } return dec.Decode(val) } @@ -367,7 +375,11 @@ func (c *Cursor) addFromBatch(sliceVal reflect.Value, elemType reflect.Type, bat } currElem := sliceVal.Index(index).Addr().Interface() - dec := getDecoder(doc, c.bsonOpts, c.registry) + var dec *bson.Decoder + dec, err = getDecoder(doc, c.bsonOpts, c.registry) + if err != nil { + return sliceVal, index, err + } err = dec.Decode(currElem) if err != nil { return sliceVal, index, err diff --git a/mongo/mongo.go b/mongo/mongo.go index 1112a297db..8cd6258e38 100644 --- a/mongo/mongo.go +++ b/mongo/mongo.go @@ -71,29 +71,36 @@ func getEncoder( } if opts != nil { + regOpts := []*bson.RegistryOpt{} if opts.ErrorOnInlineDuplicates { - enc.SetBehavior(bson.ErrorOnInlineDuplicates) + regOpts = append(regOpts, bson.ErrorOnInlineDuplicates) } if opts.IntMinSize { - enc.SetBehavior(bson.IntMinSize) + regOpts = append(regOpts, bson.IntMinSize) } if opts.NilByteSliceAsEmpty { - enc.SetBehavior(bson.NilByteSliceAsEmpty) + regOpts = append(regOpts, bson.NilByteSliceAsEmpty) } if opts.NilMapAsEmpty { - enc.SetBehavior(bson.NilMapAsEmpty) + regOpts = append(regOpts, bson.NilMapAsEmpty) } if opts.NilSliceAsEmpty { - enc.SetBehavior(bson.NilSliceAsEmpty) + regOpts = append(regOpts, bson.NilSliceAsEmpty) } if opts.OmitZeroStruct { - enc.SetBehavior(bson.OmitZeroStruct) + regOpts = append(regOpts, bson.OmitZeroStruct) } if opts.StringifyMapKeysWithFmt { - enc.SetBehavior(bson.StringifyMapKeysWithFmt) + regOpts = append(regOpts, bson.StringifyMapKeysWithFmt) } if opts.UseJSONStructTags { - enc.SetBehavior(bson.UseJSONStructTags) + regOpts = append(regOpts, bson.UseJSONStructTags) + } + for _, opt := range regOpts { + err := enc.SetBehavior(opt) + if err != nil { + return nil, err + } } } @@ -167,7 +174,11 @@ func ensureID( var id struct { ID interface{} `bson:"_id"` } - dec := getDecoder(doc, bsonOpts, registry) + var dec *bson.Decoder + dec, err = getDecoder(doc, bsonOpts, registry) + if err != nil { + return nil, nil, fmt.Errorf("error unmarshaling BSON document: %w", err) + } err = dec.Decode(&id) if err != nil { return nil, nil, fmt.Errorf("error unmarshaling BSON document: %w", err) diff --git a/mongo/single_result.go b/mongo/single_result.go index f467666167..d6223d31ee 100644 --- a/mongo/single_result.go +++ b/mongo/single_result.go @@ -73,7 +73,10 @@ func (sr *SingleResult) Decode(v interface{}) error { return sr.err } - dec := getDecoder(sr.rdr, sr.bsonOpts, sr.reg) + dec, err := getDecoder(sr.rdr, sr.bsonOpts, sr.reg) + if err != nil { + return err + } return dec.Decode(v) }