Skip to content

Commit

Permalink
GODRIVER-2796 Update ObjectIDAsHexString behavior. (#1694)
Browse files Browse the repository at this point in the history
  • Loading branch information
qingyang-hu authored Jul 25, 2024
1 parent 3a85395 commit e31ff26
Show file tree
Hide file tree
Showing 14 changed files with 129 additions and 284 deletions.
134 changes: 9 additions & 125 deletions bson/bsoncodec.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,10 @@ func (vde ValueDecoderError) Error() string {
type EncodeContext struct {
*Registry

// MinSize causes the Encoder to marshal Go integer values (int, int8, int16, int32, int64,
// minSize 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.
//
// Deprecated: Use bson.Encoder.IntMinSize instead.
MinSize bool
minSize bool

errorOnInlineDuplicates bool
stringifyMapKeysWithFmt bool
Expand All @@ -93,148 +91,34 @@ type EncodeContext struct {
useJSONStructTags bool
}

// 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.
//
// Deprecated: Use [go.mongodb.org/mongo-driver/bson.Encoder.ErrorOnInlineDuplicates] instead.
func (ec *EncodeContext) ErrorOnInlineDuplicates() {
ec.errorOnInlineDuplicates = true
}

// StringifyMapKeysWithFmt causes the Encoder to convert Go map keys to BSON document field name
// strings using fmt.Sprintf() instead of the default string conversion logic.
//
// Deprecated: Use [go.mongodb.org/mongo-driver/bson.Encoder.StringifyMapKeysWithFmt] instead.
func (ec *EncodeContext) StringifyMapKeysWithFmt() {
ec.stringifyMapKeysWithFmt = true
}

// NilMapAsEmpty causes the Encoder to marshal nil Go maps as empty BSON documents instead of BSON
// null.
//
// Deprecated: Use [go.mongodb.org/mongo-driver/bson.Encoder.NilMapAsEmpty] instead.
func (ec *EncodeContext) NilMapAsEmpty() {
ec.nilMapAsEmpty = true
}

// NilSliceAsEmpty causes the Encoder to marshal nil Go slices as empty BSON arrays instead of BSON
// null.
//
// Deprecated: Use [go.mongodb.org/mongo-driver/bson.Encoder.NilSliceAsEmpty] instead.
func (ec *EncodeContext) NilSliceAsEmpty() {
ec.nilSliceAsEmpty = true
}

// NilByteSliceAsEmpty causes the Encoder to marshal nil Go byte slices as empty BSON binary values
// instead of BSON null.
//
// Deprecated: Use [go.mongodb.org/mongo-driver/bson.Encoder.NilByteSliceAsEmpty] instead.
func (ec *EncodeContext) NilByteSliceAsEmpty() {
ec.nilByteSliceAsEmpty = true
}

// OmitZeroStruct causes the Encoder to consider the zero value for a struct (e.g. MyStruct{})
// as empty and omit it from the marshaled BSON when the "omitempty" struct tag option is set.
//
// 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.
//
// Deprecated: Use [go.mongodb.org/mongo-driver/bson.Encoder.OmitZeroStruct] instead.
func (ec *EncodeContext) OmitZeroStruct() {
ec.omitZeroStruct = true
}

// UseJSONStructTags causes the Encoder to fall back to using the "json" struct tag if a "bson"
// struct tag is not specified.
//
// Deprecated: Use [go.mongodb.org/mongo-driver/bson.Encoder.UseJSONStructTags] instead.
func (ec *EncodeContext) UseJSONStructTags() {
ec.useJSONStructTags = true
}

// DecodeContext is the contextual information required for a Codec to decode a
// value.
type DecodeContext struct {
*Registry

// Truncate, if true, instructs decoders to to truncate the fractional part of BSON "double"
// truncate, if true, instructs decoders to to truncate the fractional part of BSON "double"
// values when attempting to unmarshal them into a Go integer (int, int8, int16, int32, int64,
// uint, uint8, uint16, uint32, or uint64) struct field. The truncation logic does not apply to
// BSON "decimal128" values.
//
// Deprecated: Use bson.Decoder.AllowTruncatingDoubles instead.
Truncate bool
truncate bool

// defaultDocumentType specifies the Go type to decode top-level and nested BSON documents into. In particular, the
// usage for this field is restricted to data typed as "interface{}" or "map[string]interface{}". If DocumentType is
// set to a type that a BSON document cannot be unmarshaled into (e.g. "string"), unmarshalling will result in an
// error.
defaultDocumentType reflect.Type

binaryAsSlice bool
binaryAsSlice bool

// a false value results in a decoding error.
objectIDAsHexString bool

useJSONStructTags bool
useLocalTimeZone bool
zeroMaps bool
zeroStructs bool
}

// 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 Binary.
//
// Deprecated: Use [go.mongodb.org/mongo-driver/bson.Decoder.BinaryAsSlice] instead.
func (dc *DecodeContext) BinaryAsSlice() {
dc.binaryAsSlice = true
}

// UseJSONStructTags causes the Decoder to fall back to using the "json" struct tag if a "bson"
// struct tag is not specified.
//
// Deprecated: Use [go.mongodb.org/mongo-driver/bson.Decoder.UseJSONStructTags] instead.
func (dc *DecodeContext) UseJSONStructTags() {
dc.useJSONStructTags = true
}

// UseLocalTimeZone causes the Decoder to unmarshal time.Time values in the local timezone instead
// of the UTC timezone.
//
// Deprecated: Use [go.mongodb.org/mongo-driver/bson.Decoder.UseLocalTimeZone] instead.
func (dc *DecodeContext) UseLocalTimeZone() {
dc.useLocalTimeZone = true
}

// 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.
//
// Deprecated: Use [go.mongodb.org/mongo-driver/bson.Decoder.ZeroMaps] instead.
func (dc *DecodeContext) ZeroMaps() {
dc.zeroMaps = true
}

// 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.
//
// Deprecated: Use [go.mongodb.org/mongo-driver/bson.Decoder.ZeroStructs] instead.
func (dc *DecodeContext) ZeroStructs() {
dc.zeroStructs = true
}

// DefaultDocumentM causes the Decoder to always unmarshal documents into the M type. This
// behavior is restricted to data typed as "interface{}" or "map[string]interface{}".
//
// Deprecated: Use [go.mongodb.org/mongo-driver/bson.Decoder.DefaultDocumentM] instead.
func (dc *DecodeContext) DefaultDocumentM() {
dc.defaultDocumentType = reflect.TypeOf(M{})
}

// ValueCodec is an interface for encoding and decoding a reflect.Value.
// values.
//
// Deprecated: Use [ValueEncoder] and [ValueDecoder] instead.
type ValueCodec interface {
ValueEncoder
ValueDecoder
}

// ValueEncoder is the interface implemented by types that can encode a provided Go type to BSON.
// The value to encode is provided as a reflect.Value and a bson.ValueWriter is used within the
// EncodeValue method to actually create the BSON representation. For convenience, ValueEncoderFunc
Expand Down
46 changes: 12 additions & 34 deletions bson/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,6 @@ var decPool = sync.Pool{
type Decoder struct {
dc DecodeContext
vr ValueReader

defaultDocumentM bool

binaryAsSlice bool
useJSONStructTags bool
useLocalTimeZone bool
zeroMaps bool
zeroStructs bool
}

// NewDecoder returns a new decoder that uses the DefaultRegistry to read from vr.
Expand Down Expand Up @@ -81,25 +73,6 @@ func (d *Decoder) Decode(val interface{}) error {
return err
}

if d.defaultDocumentM {
d.dc.DefaultDocumentM()
}
if d.binaryAsSlice {
d.dc.BinaryAsSlice()
}
if d.useJSONStructTags {
d.dc.UseJSONStructTags()
}
if d.useLocalTimeZone {
d.dc.UseLocalTimeZone()
}
if d.zeroMaps {
d.dc.ZeroMaps()
}
if d.zeroStructs {
d.dc.ZeroStructs()
}

return decoder.DecodeValue(d.dc, d.vr, rval)
}

Expand All @@ -117,42 +90,47 @@ func (d *Decoder) SetRegistry(r *Registry) {
// 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{}".
func (d *Decoder) DefaultDocumentM() {
d.defaultDocumentM = true
d.dc.defaultDocumentType = reflect.TypeOf(M{})
}

// 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.
func (d *Decoder) AllowTruncatingDoubles() {
d.dc.Truncate = true
d.dc.truncate = true
}

// 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.
func (d *Decoder) BinaryAsSlice() {
d.binaryAsSlice = true
d.dc.binaryAsSlice = true
}

// ObjectIDAsHexString causes the Decoder to decode object IDs to their hex representation.
func (d *Decoder) ObjectIDAsHexString() {
d.dc.objectIDAsHexString = true
}

// UseJSONStructTags causes the Decoder to fall back to using the "json" struct tag if a "bson"
// struct tag is not specified.
func (d *Decoder) UseJSONStructTags() {
d.useJSONStructTags = true
d.dc.useJSONStructTags = true
}

// UseLocalTimeZone causes the Decoder to unmarshal time.Time values in the local timezone instead
// of the UTC timezone.
func (d *Decoder) UseLocalTimeZone() {
d.useLocalTimeZone = true
d.dc.useLocalTimeZone = true
}

// 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.
func (d *Decoder) ZeroMaps() {
d.zeroMaps = true
d.dc.zeroMaps = true
}

// 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.
func (d *Decoder) ZeroStructs() {
d.zeroStructs = true
d.dc.zeroStructs = true
}
39 changes: 39 additions & 0 deletions bson/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,10 @@ func TestDecoderConfiguration(t *testing.T) {
MyUint64 uint64
}

type objectIDTest struct {
ID string
}

type jsonStructTest struct {
StructFieldName string `json:"jsonFieldName"`
}
Expand Down Expand Up @@ -510,6 +514,21 @@ func TestDecoderConfiguration(t *testing.T) {
{Key: "myDocument", Value: M{"myString": "test value"}},
},
},
// Test that ObjectIDAsHexString causes the Decoder to decode object ID to hex.
{
description: "ObjectIDAsHexString",
configure: func(dec *Decoder) {
dec.ObjectIDAsHexString()
},
input: bsoncore.NewDocumentBuilder().
AppendObjectID("id", func() ObjectID {
id, _ := ObjectIDFromHex("5ef7fdd91c19e3222b41b839")
return id
}()).
Build(),
decodeInto: func() interface{} { return &objectIDTest{} },
want: &objectIDTest{ID: "5ef7fdd91c19e3222b41b839"},
},
// Test that UseJSONStructTags causes the Decoder to fall back to "json" struct tags if
// "bson" struct tags are not available.
{
Expand Down Expand Up @@ -588,6 +607,26 @@ func TestDecoderConfiguration(t *testing.T) {
})
}

t.Run("Decoding an object ID to string", func(t *testing.T) {
t.Parallel()

type objectIDTest struct {
ID string
}

doc := bsoncore.NewDocumentBuilder().
AppendObjectID("id", func() ObjectID {
id, _ := ObjectIDFromHex("5ef7fdd91c19e3222b41b839")
return id
}()).
Build()

dec := NewDecoder(NewValueReader(doc))

var got objectIDTest
err := dec.Decode(&got)
assert.EqualError(t, err, "error decoding key id: decoding an object ID to a non-hexadecimal string representation is not supported")
})
t.Run("DefaultDocumentM top-level", func(t *testing.T) {
t.Parallel()

Expand Down
4 changes: 2 additions & 2 deletions bson/default_value_decoders.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ func intDecodeType(dc DecodeContext, vr ValueReader, t reflect.Type) (reflect.Va
if err != nil {
return emptyValue, err
}
if !dc.Truncate && math.Floor(f64) != f64 {
if !dc.truncate && math.Floor(f64) != f64 {
return emptyValue, errCannotTruncate
}
if f64 > float64(math.MaxInt64) {
Expand Down Expand Up @@ -368,7 +368,7 @@ func floatDecodeType(dc DecodeContext, vr ValueReader, t reflect.Type) (reflect.

switch t.Kind() {
case reflect.Float32:
if !dc.Truncate && float64(float32(f)) != f {
if !dc.truncate && float64(float32(f)) != f {
return emptyValue, errCannotTruncate
}

Expand Down
8 changes: 4 additions & 4 deletions bson/default_value_decoders_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ func TestDefaultValueDecoders(t *testing.T) {
nil,
},
{
"ReadDouble (truncate)", int64(3), &DecodeContext{Truncate: true},
"ReadDouble (truncate)", int64(3), &DecodeContext{truncate: true},
&valueReaderWriter{BSONType: TypeDouble, Return: float64(3.14)}, readDouble,
nil,
},
Expand Down Expand Up @@ -419,7 +419,7 @@ func TestDefaultValueDecoders(t *testing.T) {
nil,
},
{
"ReadDouble (truncate)", uint64(3), &DecodeContext{Truncate: true},
"ReadDouble (truncate)", uint64(3), &DecodeContext{truncate: true},
&valueReaderWriter{BSONType: TypeDouble, Return: float64(3.14)}, readDouble,
nil,
},
Expand Down Expand Up @@ -670,7 +670,7 @@ func TestDefaultValueDecoders(t *testing.T) {
nil,
},
{
"float32/fast path (truncate)", float32(3.14), &DecodeContext{Truncate: true},
"float32/fast path (truncate)", float32(3.14), &DecodeContext{truncate: true},
&valueReaderWriter{BSONType: TypeDouble, Return: float64(3.14)}, readDouble,
nil,
},
Expand Down Expand Up @@ -708,7 +708,7 @@ func TestDefaultValueDecoders(t *testing.T) {
nil,
},
{
"float32/reflection path (truncate)", myfloat32(3.14), &DecodeContext{Truncate: true},
"float32/reflection path (truncate)", myfloat32(3.14), &DecodeContext{truncate: true},
&valueReaderWriter{BSONType: TypeDouble, Return: float64(3.14)}, readDouble,
nil,
},
Expand Down
2 changes: 1 addition & 1 deletion bson/default_value_encoders.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func intEncodeValue(ec EncodeContext, vw ValueWriter, val reflect.Value) error {
return vw.WriteInt64(i64)
case reflect.Int64:
i64 := val.Int()
if ec.MinSize && fitsIn32Bits(i64) {
if ec.minSize && fitsIn32Bits(i64) {
return vw.WriteInt32(int32(i64))
}
return vw.WriteInt64(i64)
Expand Down
Loading

0 comments on commit e31ff26

Please sign in to comment.