-
Notifications
You must be signed in to change notification settings - Fork 892
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
GODRIVER-2407 Add DocumentDecodeType to DecodeContext #951
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking great; the concept of a new DocumentDecodeType
on DecodeContext
makes sense to me. After discussion offline, sounds like we want to defer the "long term" solution to another ticket?
Co-authored-by: Benjamin Rewis <32186188+benjirewis@users.noreply.github.com>
bson/bsoncodec/bsoncodec.go
Outdated
|
||
// DocumentDecodeType will decode embedded documents into the defined type, rather than into primitive.D. This is | ||
// work-around for custom typing and is All-Or-None. | ||
DocumentDecodeType *reflect.Type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reflect.Type
is an interface, so we can already set it to nil
without being a pointer. Update DocumentDecodeType
to be type reflect.Type
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
bson/bsoncodec/bsoncodec.go
Outdated
|
||
// DocumentDecodeType will decode embedded documents into the defined type, rather than into primitive.D. This is a | ||
// work-around for custom typing. | ||
DocumentDecodeType *reflect.Type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering some alternative names:
DocumentType
- The option is on aDecodeContext
, so the "Decode" part is redundant. Still,DocumentType
doesn't make it clear what we're configuring.DefaultDocumentType
- Technically more correct because we'll still ignore this configuration if there is type information in theDecode
/Unmarshal
output parameter.OverrideDefaultDocumentType
- The most descriptive, but pretty wordy.
I think DefaultDocumentType
is the best option. Consider renaming DocumentDecodeType
to DefaultDocumentType
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually like DocumentType
the best. I think it reads well as part of the DecodeContext
, i.e. "For decoding context, the document type is X". What do you think? I'm definitely not married to anything, this just makes the most sense to me ATM.
bson/bsoncodec/bsoncodec.go
Outdated
@@ -123,6 +123,16 @@ type DecodeContext struct { | |||
// Ancestor is a bson.M, BSON embedded document values being decoded into an empty interface | |||
// will be decoded into a bson.M. | |||
Ancestor reflect.Type | |||
|
|||
// DocumentDecodeType will decode embedded documents into the defined type, rather than into primitive.D. This is a | |||
// work-around for custom typing. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should make this comment more descriptive to help users understand when and how to use this configuration.
E.g.
// DefaultDocumentType configures the default Go type to unmarshal top-level and nested BSON documents
// into. The DefaultDocumentType is only used if the Decoder can't determine type information based on
// the Decode output parameter (e.g. is type "interface{}" or "map[string]interface{}"). Setting
// DefaultDocumentType overrides the Ancestor field. If DefaultDocumentType is set to a type that a
// BSON document cannot be unmarshaled into (e.g. "string"), unmarshalling will result in an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, with a few tweaks. What are your thoughts?
// DocumentType 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. DocumentType overrides the Ancestor field.
@@ -123,6 +123,16 @@ type DecodeContext struct { | |||
// Ancestor is a bson.M, BSON embedded document values being decoded into an empty interface | |||
// will be decoded into a bson.M. | |||
Ancestor reflect.Type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should deprecate the Ancestor
field with this change because it doesn't really do what it describes (e.g. it's effectively ignored when the Decode
output parameter is a map[string]interface{}
).
E.g.
// Ancestor is a bson.M, BSON embedded document values being decoded into an empty interface
// will be decoded into a bson.M.
//
// Deprecated: Use DefaultDocumentType instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that the PR uses DefaultDocumentM
and DefaultDocumentD
, we should update the "Deprecated" message to say
Deprecated: Use DefaultDocumentM or DefaultDocumentD instead.
bsonFooOutType := reflect.TypeOf(bsonOut["foo"]).String() | ||
documentType := reflect.TypeOf(map[string]interface{}{}).String() | ||
assert.Equal(t, documentType, bsonFooOutType, "expected %v to equal %v", inFooType, bsonFooOutType) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add some test cases:
- Checks what happens when we
SetDocumentDecodeType
and theDecode
output parameter is typeinterface{}
. Do we expect theSetDocumentDecodeType
type to be used when decoding the top-level BSON document as well? - Check what happens when we
SetDocumentDecodeType
to astring
or something that can't represent a BSON document. Does unmarshalling return an error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- I would expect that if the output type is an empty interface, or a map pointing to an empty interface, that the logic would convert the top-level type.
- Absolutely, will do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! Awesome work.
bson/decoder_test.go
Outdated
} | ||
|
||
// We should only be able to decode data typed as interface{}, map[string]interface{}, or map[string]string. | ||
nonDocumentTypes := []reflect.Type{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome! 🧑🔧
vr bsonrw.ValueReader | ||
dc bsoncodec.DecodeContext | ||
vr bsonrw.ValueReader | ||
defaultDocumentM bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do this instead of setting directly on dc
because the Decoder
exposes a setter for the DecodeContext
: (*Decoder).SetContext
. If the user set the default document behavior and then the context, the default behavior will be overwritten. Persisting the behavior on the Decoder
allows us to set the documentType
on the decoder before the DecodeValue
call.
dec.DefaultDocumentD()
// If we didn't do it this way, this would cause the default behavior to be overwritten.
dec.SetContext(bsoncodec.DecodeContext{})
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: Consider leaving a comment about this choice in the code as well so future devs understand the decision.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few doc improvement suggestions, but looks good! 👍
bson/bsoncodec/bsoncodec.go
Outdated
// 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. DocumentType overrides the Ancestor field. | ||
DocumentType reflect.Type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: Consider adding a note about the typical values that someone would set DocumentType
to in the comment.
E.g. comment with additional clarification:
DocumentType must be a Go type that can represent any BSON document, such as a "map[string]interface{}" or "bson.M".
// DocumentType 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{}". DocumentType
// must be a Go type that can represent any BSON document, such as a "map[string]interface{}" or "bson.M". If
// DocumentType is set to a type that a BSON document cannot be unmarshaled into (e.g. "string"),
// unmarshalling will result in an error. DocumentType overrides the Ancestor field.
bson/bsoncodec/bsoncodec.go
Outdated
|
||
// DefaultDocumentM will set the defaultDocumentType as primitive.M, to be used when decoding "interface{}" and | ||
// "map[string]interface{}". | ||
func (dc *DecodeContext) DefaultDocumentM() error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, I like having identical APIs on the Decoder
and DecodeContext
.
@@ -123,6 +123,16 @@ type DecodeContext struct { | |||
// Ancestor is a bson.M, BSON embedded document values being decoded into an empty interface | |||
// will be decoded into a bson.M. | |||
Ancestor reflect.Type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that the PR uses DefaultDocumentM
and DefaultDocumentD
, we should update the "Deprecated" message to say
Deprecated: Use DefaultDocumentM or DefaultDocumentD instead.
bson/bsoncodec/bsoncodec.go
Outdated
} | ||
|
||
// DefaultDocumentM will set the defaultDocumentType as primitive.M, to be used when decoding "interface{}" and | ||
// "map[string]interface{}". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: Consider using the same doc comment as on Decoder.DefaultDocumentM
which is more clear and doesn't require knowing about the unexported field defaultDocumentType
.
bson/bsoncodec/bsoncodec.go
Outdated
} | ||
|
||
// DefaultDocumentD will set the defaultDocumentType as primitive.D, to be used when decoding "interface{}" and | ||
// "map[string]interface{}". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: Consider using the same doc comment as on Decoder.DefaultDocumentD
which is more clear and doesn't require knowing about the unexported field defaultDocumentType
.
vr bsonrw.ValueReader | ||
dc bsoncodec.DecodeContext | ||
vr bsonrw.ValueReader | ||
defaultDocumentM bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: Consider leaving a comment about this choice in the code as well so future devs understand the decision.
GODRIVER-2407
This PR creates a way for a user/client to write a document-type override to the
DecodeContext
per this JIRA comment.Work Around
Long-Term
My understanding is that we would like the encoder/decoder map to be bijective.
I believe that the canonical argument for this is that the purpose of a BSON decoder should be to decode BSON and although we support
bson.M
in go, the concept of a BSON Map per se doesn’t exist. Maps are documents, despite our driver's extension types. So to maintain bijectivity in our encoding, we should always enforce that maps encode to documents and documents decode toprimitive.D
.This could probably be resolved by removing the ancestor logic from the
MapCodec
decoder and updating the tests to reflect that embedded documents map toprimitive.D
in the default case.I don't think that the long-term solution proposed here will deprecate the "work around" changes suggested in this PR, having a work around will probably be something we want long-term as well.