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

Unmarshalling of nested structs #99

Merged
merged 26 commits into from
Oct 4, 2018
Merged

Unmarshalling of nested structs #99

merged 26 commits into from
Oct 4, 2018

Conversation

mrowdy
Copy link
Contributor

@mrowdy mrowdy commented Jul 15, 2017

This mainly addresses #49 and has nothing todo with #21

The marshalling function is able to convert a nested struct into a valid json:api representation but you can't use the output for the unmarshalPayload function since it fails when a struct is used as an attribute.

This should be possible according to the json:api spec: http://jsonapi.org/format/#document-resource-object-attributes

The PR adds this functionality with the limitation of pointers. At the moment this only works with values (as shown in the added tests)

I also had to restructure the unmarshalNode function in order to convert the attributes recursively. This should simplify the implementation of #95

@googlebot
Copy link

Thanks for your pull request. It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

📝 Please visit https://cla.developers.google.com/ to sign.

Once you've signed, please reply here (e.g. I signed it!) and we'll verify. Thanks.


  • If you've already signed a CLA, it's possible we don't have your GitHub username or you're using a different email address. Check your existing CLA data and verify that your email is set on your git commits.
  • If your company signed a CLA, they designated a Point of Contact who decides which employees are authorized to participate. You may need to contact the Point of Contact for your company and ask to be added to the group of authorized contributors. If you don't know who your Point of Contact is, direct the project maintainer to go/cla#troubleshoot.
  • In order to pass this check, please resolve this problem and have the pull request author add another comment and the bot will run again.

@mrowdy
Copy link
Contributor Author

mrowdy commented Jul 15, 2017

I signed it!

@googlebot
Copy link

CLAs look good, thanks!

@dev-mush
Copy link

dev-mush commented Aug 4, 2017

hello @Slemgrim , I was having the same issues and got into your solution, I'm encountering however a few problems and before diving deep into your (and google's) code, I might use some feedback to know how are you using the tool.

There are two main problems I'm encountering:

problem 1 (tl;dr: how do you use the tags?)

Do you tag an attribute with the json or jsonapi tag for a nested struct (or well, an attribute of the main jsonapi object which is a struct)?
I found that if I use the jsonapi tag like the following:

package funnymodel 

type Person struct{
        ID string `jsonapi:"primary,persons"`
	Name  string `jsonapi:"attr,name`
	Contacts []Contact `jsonapi:"attr,contacts"`
}

type Contact struct {
	Name  string `jsonapi:"attr,name,omitempty"`
	Value string `jsonapi:"attr,value,omitempty"`
}

when marshalling, the fields of the Contact struct, they get translated into json with the struct's attribute names rather than the jsonapi tag values (i.e. : [{"Name":"work","Value":"fakemail@fakeworld.com"}] instead of [{"name":"work","value":"fakemail@fakeworld.com"}], notice the capital letters? I didn't for a long time and was getting crazy -_-).

If I use the json tag instead, everything seems smooth marshalling, but the unmarshalling fails, it basically skips the attributes and initialises them to their default empty value.
A mix of the two, meaning to put both the json and jsonapi tag, seems to work ok: I get to serialize and deserialize correctly, but it doesn't feel right to me.
I think the ideal would be to use just the json tag alone in nested structs, since there is no need (and it would be wrong) to specify jsonapi relations and other things in an attribute of an object.

problem 2

If I set omitemptyon a struct array, like on the []Contactsof the previous example, I get the following panic, wether the array is empty or not: panic: runtime error: comparing uncomparable type []funnymodel.Contact.
I assume the problem must be in thevisitModelNode() function, but it's quite a hell of a long function and I didn't have time to debug it yet.
If I cut away the omitempty tag, the attribute gets parsed to json as "null" when it is empty, like: "contacts":null, despite it is not a pointer and should be translated as "contacts":[] or omitted entirely (with the omitempty).
Right now I don't have enough time to deep dive into the visitModelNode function and see why the panic occurs in first place (deadlines, you know...) but as soon as I'll be more free, I'll be glad to help in case you won't figure it out by yourself in the meanwhile.

Thanks for your solution anyway, it is saving me some headaches already, hope to hear from you soon ;).

@mrowdy
Copy link
Contributor Author

mrowdy commented Aug 5, 2017

@dev-mush you're right. i don't see a reason why we should use jsonapi annotation inside nested structs. I haven't thought about that before. I try to update the PR in the next days.

Can you create a separate Issue for the second problem?

@mrowdy
Copy link
Contributor Author

mrowdy commented Aug 14, 2017

@dev-mush i changed the PR to work with json annotation instead of json:api. Does the solution work for you?

mrowdy added a commit to mrowdy/styx that referenced this pull request Aug 15, 2017
we're waiting for google/jsonapi to implement nested structs or to accept this PR
google/jsonapi#99

We need nested structs for the upcoming mail endpoint
Copy link
Contributor

@shwoodard shwoodard left a comment

Choose a reason for hiding this comment

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

I like this PR. It offers some much needed in addition to its intent. Can you add more informative test failures? Like expected x got y instead of just, it failed.

@shwoodard
Copy link
Contributor

@Slemgrim I'd really like to merge this, especially because it introduces a lot of much needed cleanup. A couple of questions,

  1. Is there a fundamental limitation preventing supporting ptr declared nested structs? If not, can you add support?
  2. Can you augment your test failures to include expected vs actual outcomes so that the tests are easier to debug?

I'd like to merge this asap so that we can avoid conflicts with other PRs.

Thanks in advance, Sam

tests should have an expected outcome vs. an actual outcome to make them easier to debug
@mrowdy
Copy link
Contributor Author

mrowdy commented Nov 21, 2017

@shwoodard I changed the tests to display expected vs. actual outcome.

About the support for nested ptr:
I think it should be possible but I had some difficulties with go's reflection system. I'd be happy if someone with more experience can add this feature.

@shwoodard
Copy link
Contributor

@Slemgrim I'll look into it. The key is the use of the .Elem() method.

https://golang.org/pkg/reflect/#Value.Elem

@msabramo
Copy link
Contributor

I opened a PR to @Slemgrim's branch that makes nested struct pointers work for me: mrowdy#3

to aid in troubleshooting.

Before:

```
Pointer type in struct is not supported
```

After:

```
jsonapi: Can't unmarshal true (bool) to struct field `Name`, which is a pointer to `string`
```
@shwoodard
Copy link
Contributor

We're working on reviewing this CL now. We're going to merge this before any others. Everyone else who has open PR's please rebase on top of this branch or wait for it to be merged within a week or two.

Copy link
Contributor

@shwoodard shwoodard left a comment

Choose a reason for hiding this comment

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

Please fix all lines to 80 chars. Thanks for the much needed cleanup help!

request.go Outdated
// ErrInvalidType is returned when the given type is incompatible with the expected type.
ErrInvalidType = errors.New("Invalid type provided") // I wish we used punctuation.
)

// ErrUnsupportedPtrType is returned when the Struct field was a pointer but
// the JSON value was of a different type
func ErrUnsupportedPtrType(rf reflect.Value, t reflect.Type, structField reflect.StructField) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please refactor this so as to use the error interface, the standard Go pattern for errors that have state.

  1. Create a public/exported struct called called ErrUnsupportedPtrType with private (lower-case) fields rf, t, and structField as members.
  2. Implement func (eupt ErrUnsupportedPtrType) Error() string to complete the error interface, See, https://golang.org/src/builtin/builtin.go?h=error#L255.
  3. Create a private/unexported function called newErrUnsupportedPtrType and take the formal params that are listed above, reflect.Value, reflect.Type, reflect.StructField and returns a ErrUnsupportedPtrType, for convenience.
  4. Use return newErrUnsupportedPtrType(...) in place of your exported function here.

request.go Outdated
@@ -548,3 +374,239 @@ func assign(field, value reflect.Value) {
field.Set(reflect.Indirect(value))
}
}

func unmarshalAttribute(attribute interface{}, args []string, structField reflect.StructField, fieldValue reflect.Value) (value reflect.Value, err error) {

Copy link
Contributor

Choose a reason for hiding this comment

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

Remove whitespace.

request.go Outdated
if kind.String() != "" && kind.String() != typeName {
typeName = fmt.Sprintf("%s (%s)", typeName, kind.String())
}
return fmt.Errorf(
Copy link
Contributor

Choose a reason for hiding this comment

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

When you do the change described above, you'll need to change this to fmt.Sprintf to return string.

request.go Outdated
@@ -118,6 +129,7 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
}

func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) {

Copy link
Contributor

Choose a reason for hiding this comment

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

Remove whitespace

request.go Outdated
}

func handleTime(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) {

Copy link
Contributor

Choose a reason for hiding this comment

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

Remove whitepace.

request.go Outdated
return numericValue, nil
}

func handlePointer(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value, structField reflect.StructField) (reflect.Value, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

80 chars, etc

request.go Show resolved Hide resolved
request.go Outdated
func handleStruct(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) {
model := reflect.New(fieldValue.Type())

var er error
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove

request.go Outdated

var er error

data, er := json.Marshal(attribute)
Copy link
Contributor

Choose a reason for hiding this comment

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

Rename err and update all below.

request_test.go Show resolved Hide resolved
@shwoodard
Copy link
Contributor

@Slemgrim, Thank you so much for your contributions! I look forward to getting this merged soon.

Copy link
Contributor Author

@mrowdy mrowdy left a comment

Choose a reason for hiding this comment

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

@shwoodard i changed all the requested things except for the switch. Maybe you can spot why it doesn't work if the two primitive types are added.

Thanks you for the feedback!

request.go Show resolved Hide resolved
@msabramo
Copy link
Contributor

Awesome! Looking forward to this!

@driquet
Copy link

driquet commented Mar 20, 2018

I look forward to this PR being merged soon as well! Greatly needed here, good job!

@korfuri
Copy link

korfuri commented Apr 11, 2018

Awesome work, thank you! Is there an estimate on when this could be merged? I'd love to be able to use this.

@valentindotxyz
Copy link

Hoping it will be merged soon too! 🙏🏻

@mrowdy
Copy link
Contributor Author

mrowdy commented Apr 14, 2018

@shwoodard is there anything i can do to get this merged soon?

@nathanKramer
Copy link

Looking forward to this being merged :)

@svanharmelen
Copy link

@shwoodard and @aren55555 I would like to join the list of people that would be really happy if this PR can get merged! It seems the authors and yourselves have given it quite some attention, so would be really awesome if you could give it a last push in order to merge the PR 😃Thanks!

@svanharmelen
Copy link

@shwoodard and @aren55555 sorry to bother you, but is there anyone who could give this project some TLC? It seems the last activity from any of it's maintainers is about 3 months ago, while there are a few very valuable PRs pending that solve quite some issues and/or use cases. So would be really great if someone could review and/or merge a few of them so we don't end up will everyone having it's own custom fork and lose the ability to share back 😏Thanks!

@msabramo
Copy link
Contributor

Maybe if the current maintainers are too busy to tend to this project, perhaps they might consider adding a few more committers?

@marcgiovannoni
Copy link

Any update on this?

@svanharmelen
Copy link

Anything that can be done to move this forward?

models_test.go Outdated
}

type Team struct {
Name string `json:"name"`
Copy link
Contributor

@aren55555 aren55555 Jul 25, 2018

Choose a reason for hiding this comment

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

I've read the comment @dev-mush left.

I think all these tags (even for a nested resource) should all be jsonapi rather than json. Why? What if you want to support serializations for both JSON and JSONAPI; you'd want separate tags for each. I'd recommend just using jsonapi:"attr,names" even for these nested struct's fields.

The rest of this PR seems OK to me. @shwoodard perhaps you can also take a quick look?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@aren55555 according to the spec attributes need to be valid json but can't be a json:api object since they are not allowed to have links or relationships.

Maybe I get this totaly wrong but this would allow the following:

{
	"data": {
		"id": "1324",
		"type": "article",
		"attributes": {
			"title": "The best article",
			"author": {
				"id": "18",
				"type": "author",
				"attributes": {
					"name": "Slemgrim"
				}
			}
		}
	}
}

where you also could do this:


{
	"data": {
		"id": "1324",
		"type": "article",
		"attributes": {
			"title": "The best article",
		}
	},
	"relationships": {
	    "author": {
	    	"data": {"id": "18", "type": "author"}
	    }
	},
	"included": [{
		"id": "18",
		"type": "author",
		"attributes": {
			"name": "Slemgrim"
		}
	}]
}

Copy link
Contributor

@aren55555 aren55555 Jul 25, 2018

Choose a reason for hiding this comment

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

Could the unmarshal/marshal not just return an error if anything other than an attr is serialized/deserialized? Or it could ignore any tag that was not an attr?

Copy link
Contributor

Choose a reason for hiding this comment

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

@Slemgrim bump on question from @aren55555 . If we can resolve this, we'll merge this immediately.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@aren55555 @shwoodard I tried to look into this today. The changes required to allow "jsonapi" marshalling for nested attributes, seem non-trivial with the current architecture. At least non-trivial to me.

I'm happy to spend my time on this if someone could give me an example where this would be needed.

The jsonapi spec does not allow the fields "id, type, relationship, links" as attribute names. In other words: it doesn't allow to have a resource inside a resource. Allowing to tag the attributes of a non jsonapi resource with jsonapi tags would be missleading. It's like saying: "hey, you can name this json value jsonapi even though it will always be a normal json"

If you insist on this without further explanation then maybe one of the other contributers of this PR are willing to implement this. (@msabramo)

Copy link
Contributor

@shwoodard shwoodard Sep 19, 2018

Choose a reason for hiding this comment

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

Could the solution be to validate that id, type, and links are missing from nested structs? Maybe we should allow relationships and process the data to be in the "included" part of the response?

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with @aren55555 that we should use jsonapi for all properties serialized or deserialized by this lib.

@adamlc
Copy link

adamlc commented Aug 29, 2018

Any more updates on this? We really need this :(

Use jsonapi struct tags for nested attrs
@shwoodard
Copy link
Contributor

I'm ready to merge the PR. I need all contributors to confirm CLA(s).

@msabramo I think you're consent may be missing.

@PothulaAdiseshu
Copy link

@Slemgrim @dev-mush I could still face Problem 1 in the repo.. Is it because of overriding or am i missing something? can anyone suggest on this ?

Integralist pushed a commit to fastly/go-fastly that referenced this pull request Feb 15, 2021
* Change TLS certificate `name` attribute to be optional

Custom TLS certificate without names default to the certificates common name or first SAN entry

* Not use uint pointers in ListPrivateKeysInput struct

This was causing problems when trying to use the page[number] parameter so I brought it more in line with the other methods.

* Change TLS Activation `configuration` attribute to be optional (#28)

Activations without the configuration specified default to the default configuration

* Add TLS domains endpoint support

* Drop use of reflection to build request parameters

* Add documentation

* TLS Subscriptions endpoints (#30)

* Add ListTLSSubscriptions function

* Add CreateTLSSubscriptions function

* Add GetTLSSubscriptions function

* Add DeleteTLSSubscriptions function

* Add integration test with all endpoints

* Add TLS Authorizations to TLS Subscriptions (#31)

I had a lot of trouble deserialising the API response into appropriate structs. Initially I could only get it to work by deserialising into a map[string]interface{} but this didn't give the rich typing I wanted to return to the user. After a long time digging through the JSONAPI implementation and an associated PR to allow nested structs (google/jsonapi#99), I found that the go-fastly dependency was on an older version of the library before this PR came in. Updating the version, and playing around some more with the slice of structs instead of slice of pointers, finally got this to work.

Furthermore I found that the Header in ListTLSSubscriptions needed to include the "Accept" header for "application/vnd.api+json" so I added it back in. I previously removed it as it didn't seem to do anything with the Filter and Pagination parameters, but it seems to be required for Include to work.

* Avoid use pointers and uint in ListBulkCertificatesInput struct

* Add allow_untrusted_root field to platform TLS

* Get Certificate ID for TLS Subscriptions when available

The certificate can't really be queried in the same way as the custom TLS certificate, but the ID will be present in the TLS activation automatically created for the TLS subscription, so can help filter for this activation.

* Remove (TLS)\w* from field names of TLS structs

* Add CommonName to Create, Get, and List TLSSubscription functions

* Add "force" flag to TLS Subscription deletion

Allows deleting a subscription with active domains; a potentially
dangerous operation if the subscription handles production traffic. By
using a flag, the user opts in to this risky behaviour and takes
responsibility for knowing what they are doing.

* go mod tidy

* Add comments describing new structs and function

* Document TLSAuthorizations struct

Co-authored-by: Will May <will.j.may@gmail.com>
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.