Skip to content
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

Merged
merged 12 commits into from
May 26, 2022

Conversation

prestonvasquez
Copy link
Collaborator

@prestonvasquez prestonvasquez commented May 17, 2022

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

dec, err := NewDecoder(bsonrw.NewBSONDocumentReader(bytes))
if err != nil {
	t.Fatal(err)
}

// This will default empty interfaces into primitive.D.
dec.DefaultDocumentD()

Long-Term

My understanding is that we would like the encoder/decoder map to be bijective.

        encoder                decoder
go type ───────► bsontype.Type ───────► driver type

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 to primitive.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 to primitive.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.

@prestonvasquez prestonvasquez marked this pull request as ready for review May 17, 2022 19:39
Copy link
Contributor

@benjirewis benjirewis left a 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?

bson/bsoncodec/bsoncodec.go Outdated Show resolved Hide resolved
bson/bsoncodec/empty_interface_codec.go Outdated Show resolved Hide resolved
bson/bsoncodec/empty_interface_codec.go Outdated Show resolved Hide resolved
bson/bsoncodec/map_codec.go Outdated Show resolved Hide resolved
bson/decoder_test.go Show resolved Hide resolved
bson/bsoncodec/bsoncodec.go Outdated Show resolved Hide resolved
bson/decoder_test.go Outdated Show resolved Hide resolved
prestonvasquez and others added 2 commits May 19, 2022 16:17
Co-authored-by: Benjamin Rewis <32186188+benjirewis@users.noreply.github.com>

// 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
Copy link
Collaborator

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!


// 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
Copy link
Collaborator

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 a DecodeContext, 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 the Decode/Unmarshal output parameter.
  • OverrideDefaultDocumentType - The most descriptive, but pretty wordy.

I think DefaultDocumentType is the best option. Consider renaming DocumentDecodeType to DefaultDocumentType.

Copy link
Collaborator Author

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.

@@ -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.
Copy link
Collaborator

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.

Copy link
Collaborator Author

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
Copy link
Collaborator

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Copy link
Collaborator

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)
})
Copy link
Collaborator

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 the Decode output parameter is type interface{}. Do we expect the SetDocumentDecodeType type to be used when decoding the top-level BSON document as well?
  • Check what happens when we SetDocumentDecodeType to a string or something that can't represent a BSON document. Does unmarshalling return an error?

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Copy link
Contributor

@benjirewis benjirewis left a 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.

}

// We should only be able to decode data typed as interface{}, map[string]interface{}, or map[string]string.
nonDocumentTypes := []reflect.Type{
Copy link
Contributor

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
Copy link
Collaborator Author

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{})

Copy link
Collaborator

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.

Copy link
Collaborator

@matthewdale matthewdale left a 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! 👍

// 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
Copy link
Collaborator

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.


// DefaultDocumentM will set the defaultDocumentType as primitive.M, to be used when decoding "interface{}" and
// "map[string]interface{}".
func (dc *DecodeContext) DefaultDocumentM() error {
Copy link
Collaborator

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
Copy link
Collaborator

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.

}

// DefaultDocumentM will set the defaultDocumentType as primitive.M, to be used when decoding "interface{}" and
// "map[string]interface{}".
Copy link
Collaborator

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.

}

// DefaultDocumentD will set the defaultDocumentType as primitive.D, to be used when decoding "interface{}" and
// "map[string]interface{}".
Copy link
Collaborator

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
Copy link
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants