-
Notifications
You must be signed in to change notification settings - Fork 114
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
Refactor argument-handling to use a struct #103
Conversation
Hmm, I realized this actually makes some more breaking changes, because my implementation of |
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.
So that was my concern... that it is "breaking"-ish, which I guess if fine before our blog post and initial release version, but I'd like to do that sooner rather than later, and be more sure we don't need to again if we do this.
Yeah, I think if we had more users I'd be more inclined to live with the existing behavior, but now's a good time to fix the inconsistency before anyone cares. (FWIW, this appears to be something that |
This is useful if you want to bind to a type you don't control (or use for other things) but need different serialization than its default. This is a feature gqlgen has and we've found it very useful. For example, in webapp we want to bind `DateTime` to `time.Time`, but its default serialization is not compatible with Python, so currently we have to bind to a wrapper type and cast all over the place, which is exactly the sort of boilerplate genqlient is supposed to avoid. For unmarshaling, the implementation basically just follows the existing support for abstract types; instead of calling our own generated helper, we now call your specified function. This required some refactoring to abstract the handling of custom unmarshalers generally from abstract types specifically, and to wire in not only the unmarshaler-name but also the `generator` (in order to compute the right import alias). For marshaling, I had to implement all that stuff over again; it's mostly parallel to unmarshaling (and I made a few minor changes to unmarshaling to make the two more parallel). Luckily, after #103 I at least only had to do it once, rather than implementing the same functionality for arguments and for input-type fields. It was still quite a bit of code; I didn't try to be quite as completionist about the tests as with unmarshal but still had to add a few. Issue: #38 Test plan: make check Reviewers: marksandstrom, steve, adam, jvoll, miguel, mahtab
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 don't feel like we need to worry about breaking anyone at this point, plus the change is well documented.
generate/operation.go.tmpl
Outdated
{{.GoName}} {{.GoType}}, | ||
{{- if .Input -}} | ||
{{- range .Input.Fields -}} | ||
{{/* the GraphQL name is here the user-specified variable-name */ -}} |
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.
typo? "is here" => "here is"
graphql/client.go
Outdated
// Typically, GraphQL APIs will accept a JSON payload of the form | ||
// `query myQuery { myField }`. variables contains a JSON-marshalable | ||
// value containing the variables to be sent along with the query, | ||
// or may be nil if there are none. Typically, GraphQL APIs wills |
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.
// or may be nil if there are none. Typically, GraphQL APIs wills | |
// or may be nil if there are none. Typically, GraphQL APIs will |
generate/operation.go.tmpl
Outdated
{{if not .Options.GetOmitempty -}} | ||
"{{.GraphQLName}}": {{.GoName}}, | ||
{{- if .Input -}} | ||
{{/* __input is not a legal variable name in GraphQL */ -}} |
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.
It might be helpful to be more explicit in this comment. Something like "__input won't clash with arguments to this function since all argument names are valid GraphQL variable names and __input isn't a legal variable name in GraphQL".
generate/convert.go
Outdated
// note prefix is ignored here (see generator.typeName), as is | ||
// selectionSet (for input types we use the whole thing). |
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 agree with the comment in the code below that it's a bit confusing to pass around arguments that aren't used. It may be helpful to explicitly say that selection sets don't apply to input objects, and input types may only be scalars, enums or (recursively) other input objects. A mention of ast.InputObject in convertDefinition might also be helpful.
Also, I think generator.typeName is a dangling reference? (Here and below in convertDefinition.)
In this commit I refactor the argument-generation logic to move most of the code out of the template and into the type-generator. This logic predates #51, and I didn't think to update it there, but I think it benefits from similar treatment, for similar reasons. Specifically, the main change is to treat variables as another struct type we can generate, rather than handling them inline as a `map[string]interface{}`. Users still pass them the same way, but instead of putting them into a `map[string]interface{}` and JSONifying that, we generate a struct and put them there. This turns out to simplify things quite a lot, because we already have a lot of code to generate types. Notably, the omitempty code goes from a dozen lines to basically two, and fixes a bug (#43) in the process, because now that we have a struct, `json.Marshal` will do our work for us! (And, once we have syntax for it (#14), we'll be able to handle field-level omitempty basically for free.) More importantly, it will simplify custom marshalers (#38, forthcoming) significantly, since we do all that logic at the containing-struct level, but will need to apply it to arguments. It does require one breaking change, for folks implementing the `graphql.Client` API (rather than just calling `NewClient`): we now pass them variables as an `interface{}` rather than a `map[string]interface{}`. For most callers, including Khan/webapp, this is basically a one-line change to the signature of their `MakeRequest`. Issue: #38 Issue: #43 Test plan: make check Reviewers: marksandstrom, steve, jvoll, mahtab, adam, miguel
d344208
to
7794c1b
Compare
This is useful if you want to bind to a type you don't control (or use for other things) but need different serialization than its default. This is a feature gqlgen has and we've found it very useful. For example, in webapp we want to bind `DateTime` to `time.Time`, but its default serialization is not compatible with Python, so currently we have to bind to a wrapper type and cast all over the place, which is exactly the sort of boilerplate genqlient is supposed to avoid. For unmarshaling, the implementation basically just follows the existing support for abstract types; instead of calling our own generated helper, we now call your specified function. This required some refactoring to abstract the handling of custom unmarshalers generally from abstract types specifically, and to wire in not only the unmarshaler-name but also the `generator` (in order to compute the right import alias). For marshaling, I had to implement all that stuff over again; it's mostly parallel to unmarshaling (and I made a few minor changes to unmarshaling to make the two more parallel). Luckily, after #103 I at least only had to do it once, rather than implementing the same functionality for arguments and for input-type fields. It was still quite a bit of code; I didn't try to be quite as completionist about the tests as with unmarshal but still had to add a few. Issue: #38 Test plan: make check Reviewers: marksandstrom, steve, adam, jvoll, miguel, mahtab
This is useful if you want to bind to a type you don't control (or use for other things) but need different serialization than its default. This is a feature gqlgen has and we've found it very useful. For example, in webapp we want to bind `DateTime` to `time.Time`, but its default serialization is not compatible with Python, so currently we have to bind to a wrapper type and cast all over the place, which is exactly the sort of boilerplate genqlient is supposed to avoid. For unmarshaling, the implementation basically just follows the existing support for abstract types; instead of calling our own generated helper, we now call your specified function. This required some refactoring to abstract the handling of custom unmarshalers generally from abstract types specifically, and to wire in not only the unmarshaler-name but also the `generator` (in order to compute the right import alias). For marshaling, I had to implement all that stuff over again; it's mostly parallel to unmarshaling (and I made a few minor changes to unmarshaling to make the two more parallel). Luckily, after #103 I at least only had to do it once, rather than implementing the same functionality for arguments and for input-type fields. It was still quite a bit of code; I didn't try to be quite as completionist about the tests as with unmarshal but still had to add a few. Issue: #38 Test plan: make check Reviewers: marksandstrom, steve, adam, jvoll, miguel, mahtab
## Summary: This is useful if you want to bind to a type you don't control (or use for other things) but need different serialization than its default. This is a feature gqlgen has and we've found it very useful. For example, in webapp we want to bind `DateTime` to `time.Time`, but its default serialization is not compatible with Python, so currently we have to bind to a wrapper type and cast all over the place, which is exactly the sort of boilerplate genqlient is supposed to avoid. For unmarshaling, the implementation basically just follows the existing support for abstract types; instead of calling our own generated helper, we now call your specified function. This required some refactoring to abstract the handling of custom unmarshalers generally from abstract types specifically, and to wire in not only the unmarshaler-name but also the `generator` (in order to compute the right import alias). For marshaling, I had to implement all that stuff over again; it's mostly parallel to unmarshaling (and I made a few minor changes to unmarshaling to make the two more parallel). Luckily, after #103 I at least only had to do it once, rather than implementing the same functionality for arguments and for input-type fields. It was still quite a bit of code; I didn't try to be quite as completionist about the tests as with unmarshal but still had to add a few. Issue: #38 ## Test plan: make check Author: benjaminjkraft Reviewers: StevenACoffman, dnerdy, benjaminjkraft, aberkan, jvoll, mahtabsabet, MiguelCastillo Required Reviewers: Approved By: StevenACoffman, dnerdy Checks: ✅ Test (1.17), ✅ Test (1.16), ✅ Test (1.15), ✅ Test (1.14), ✅ Lint, ✅ Test (1.17), ✅ Test (1.16), ✅ Test (1.15), ✅ Test (1.14), ✅ Lint Pull Request URL: #104
Summary:
In this commit I refactor the argument-generation logic to move most of
the code out of the template and into the type-generator. This logic
predates #51, and I didn't think to update it there, but I think it
benefits from similar treatment, for similar reasons.
Specifically, the main change is to treat variables as another struct
type we can generate, rather than handling them inline as a
map[string]interface{}
. Users still pass them the same way, butinstead of putting them into a
map[string]interface{}
and JSONifyingthat, we generate a struct and put them there.
This turns out to simplify things quite a lot, because we already have a
lot of code to generate types. Notably, the omitempty code goes from a
dozen lines to basically two, and fixes a bug (#43) in the process,
because now that we have a struct,
json.Marshal
will do our work forus! (And, once we have syntax for it (#14), we'll be able to handle
field-level omitempty basically for free.) More importantly, it will
simplify custom marshalers (#38, forthcoming) significantly, since we do
all that logic at the containing-struct level, but will need to apply it
to arguments.
It does require two breaking changes:
graphql.Client
API (rather than justcalling
NewClient
): we now pass them variables as aninterface{}
rather than a
map[string]interface{}
. For most callers, includingKhan/webapp, this is basically a one-line change to the signature of
their
MakeRequest
, and it should be a lot more future-proof.omitempty
option has changed to matchthat of
encoding/json
, in particular it now never considers structs"empty". The difference was never intentional (I just didn't realize
that behavior of
encoding/json
); arguably our behavior was moreuseful but I think that's outweighed by the value of consistency with
encoding/json
as well as the simpler and more correctimplementation (fixing omitempty generates broken code for structs #43 is actually quite nontrivial otherwise).
Once we have custom unmarshaler support (Allow mapping directly to a type but not using its standard JSON serialization #38), users will be able to
map a zero value to JSON null if they wish, which is mostly if not
entirely equivalent for GraphQL's purposes.
Issue: #38
Issue: #43
Test plan:
make check