-
Notifications
You must be signed in to change notification settings - Fork 2
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
feat: support custom marshaling and unmarshaling for attributes #18
Conversation
Well!! 👀 Ok, so I spent some time trying to get my example scrap to work with this patch. It didn't quite make it across the finish line!
The issue here seems to be something like:
At this point, I took a stab at implementing that, and immediately got lost in a spiky thicket of Anyway, hopefully that's useful despite me not having a complete answer to deliver. If it'd be helpful, I can try and clean up that gist to make a more legible test case; it's descended from my initial research into this space, so it's a bit of a mess. |
WAIT. Actually, I think I figured it out. func handleCustom(attribute interface{}, fieldValue reflect.Value) (reflect.Value, error) {
// Re-create the raw json representation of whatever we got over the wire --
// could be string, bool, Object, etc. etc. etc., but we want json bytes
// so we can take another crack at it.
data, err := json.Marshal(attribute)
if err != nil {
return reflect.Value{}, err
}
// Get something to serialize into, based on what the field is intended to
// be. (I think fieldValue is a wrapped instance of the field type's zero
// value... which, if the field's a pointer, is pointing to nil instead of
// allocated memory, so we're better off starting from scratch than trying
// to actually use it.)
var model reflect.Value
if fieldValue.Kind() == reflect.Pointer {
// We want the value type.
model = reflect.New(fieldValue.Type().Elem())
} else {
model = reflect.New(fieldValue.Type())
}
ref := model.Interface() // a type-erased pointer to the inner value, IIUC
if err := json.Unmarshal(data, ref); err != nil {
return reflect.Value{}, err
}
// Interestingly, we don't have to worry about whether the destination field
// is a pointer or a value; something downstream of here just handles that
// for us.
return model, nil
} And then in unmarshalAttribute, you need to do this before all the rest of the dispatch options: // Handle field with arbitrary custom unmarshal logic
if _, ok := fieldValue.Interface().(Unmarshaler); ok {
value, err = handleCustom(attribute, fieldValue)
return
} |
I'm pretty sure this isn't quite the direction we want to go, at least for the moment. A couple of thoughts:
To the latter point: we don't need to grant users complete marshal customizability to support the intent of hashicorp/go-tfe#786 in an alternative way. Essentially all we want to do there is have some conditional mechanism for marshaling a struct field when nil. |
Right — I guess I didn't say so explicitly, but I don't think defining custom marshaling for an entire jsonapi resource makes sense for go-tfe's use case. For that, we just want optional custom logic at the attribute level. (But, I think the thing I pasted is pretty close to getting there! It works in my experimental code.) |
@@ -589,6 +593,15 @@ func unmarshalAttribute( | |||
value = reflect.ValueOf(attribute) | |||
fieldType := structField.Type | |||
|
|||
i := reflect.TypeOf((*AttributeUnmarshaler)(nil)).Elem() |
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.
Can this be a package level var so it doesn't have to be evaluated on every attribute?
superceded by #21 |
WIP
This PR attempts support for custom marshaling and unmarshaling for resource attributes.
The
AttributeMarshaler
andAttributeUnmarshaler
interfaces provide methods that allow custom logic at marshal/unmarshal time.This may prove useful for a related PR: hashicorp/go-tfe#786
cc @nfagerlund @brandonc
Example use
The
UnsetableTime
type below implements theAttributeMarshaler
andAttributeUnmarshaler
interfaces. The methods in these interfaces allow complex attribute values to be transformed to simple values during marshaling:Now you can use a pointer to the custom type in place of a primitive value like
*time.Time
in the payload struct.omitempty
still works as expected:Background
Certain HTTP API designs can lead to friction when wrapped with a Go SDK. A common case is an API endpoint whose request payload contains one or more optional attributes that, when included, communicate the users' intent to update their values:
Example PATCH payload for enabling
auto_destroy_at
The payload above will update the name of the model and enable the
auto_destroy_at
toggle, while also providing a value for it to use. To unsetauto_destroy_at
, anull
value can be passed for the attribute.Example PATCH payload for disabling
auto_destroy_at
Omitting the attribute from the payload allows the model to keep whatever value has been previously set.
jsonapi
allows use of theomitempty
struct tag to create payloads that optionally omit attributes when marshaling if they have a zero value:Unfortunately, the underlying API uses a JSON
null
for disabling the toggle, so using a correspondingnil
value will cause the attribute to be omitted.Notes/Questions
encoding/json
, but they are based oninterface{}
rather thanbyte[]
. They effectively act as hooks that allow custom transformation logic during the marshaling and unmarshaling of attribute values. Is there a better verb thanMarshal
for the names of these interfaces?jsonapi
library after a custom value is returned formMarshalAttribute
. This also allows the existing struct-tag-configured date formatting logic to be applied to the transformed value without the need for re-implementation. This makes it easier to implement custom marshaling logic, but it may be harder to document and understand. A similar solution may be achievable for the unmarshaling case, but hasn't been included here.