Add support for binding with a custom marshal/unmarshal function (#104)

## 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: https://github.com/Khan/genqlient/issues/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: https://github.com/Khan/genqlient/pull/104
This commit is contained in:
Ben Kraft
2021-09-24 11:16:01 -07:00
committed by GitHub
parent 5995653583
commit 8de55d352e
42 changed files with 1900 additions and 455 deletions
+337 -77
View File
@@ -6,8 +6,10 @@ import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/Khan/genqlient/graphql"
"github.com/Khan/genqlient/internal/testutil"
)
// AnimalFields includes the GraphQL fields of Animal requested by the fragment AnimalFields.
@@ -19,6 +21,10 @@ type AnimalFields struct {
func (v *AnimalFields) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*AnimalFields
Owner json.RawMessage `json:"owner"`
@@ -32,10 +38,10 @@ func (v *AnimalFields) UnmarshalJSON(b []byte) error {
}
{
target := &v.Owner
raw := firstPass.Owner
dst := &v.Owner
src := firstPass.Owner
err = __unmarshalAnimalFieldsOwnerBeing(
target, raw)
src, dst)
if err != nil {
return fmt.Errorf(
"Unable to unmarshal AnimalFields.Owner: %w", err)
@@ -84,15 +90,15 @@ func (v *AnimalFieldsOwnerAnimal) GetTypename() string { return v.Typename }
// GetId is a part of, and documented with, the interface AnimalFieldsOwnerBeing.
func (v *AnimalFieldsOwnerAnimal) GetId() string { return v.Id }
func __unmarshalAnimalFieldsOwnerBeing(v *AnimalFieldsOwnerBeing, m json.RawMessage) error {
if string(m) == "null" {
func __unmarshalAnimalFieldsOwnerBeing(b []byte, v *AnimalFieldsOwnerBeing) error {
if string(b) == "null" {
return nil
}
var tn struct {
TypeName string `json:"__typename"`
}
err := json.Unmarshal(m, &tn)
err := json.Unmarshal(b, &tn)
if err != nil {
return err
}
@@ -100,10 +106,10 @@ func __unmarshalAnimalFieldsOwnerBeing(v *AnimalFieldsOwnerBeing, m json.RawMess
switch tn.TypeName {
case "User":
*v = new(AnimalFieldsOwnerUser)
return json.Unmarshal(m, *v)
return json.Unmarshal(b, *v)
case "Animal":
*v = new(AnimalFieldsOwnerAnimal)
return json.Unmarshal(m, *v)
return json.Unmarshal(b, *v)
case "":
return fmt.Errorf(
"Response was missing Being.__typename")
@@ -123,6 +129,10 @@ type AnimalFieldsOwnerUser struct {
func (v *AnimalFieldsOwnerUser) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*AnimalFieldsOwnerUser
graphql.NoUnmarshalJSON
@@ -162,15 +172,15 @@ func (v *LuckyFieldsUser) implementsGraphQLInterfaceLuckyFields() {}
// GetLuckyNumber is a part of, and documented with, the interface LuckyFields.
func (v *LuckyFieldsUser) GetLuckyNumber() int { return v.LuckyNumber }
func __unmarshalLuckyFields(v *LuckyFields, m json.RawMessage) error {
if string(m) == "null" {
func __unmarshalLuckyFields(b []byte, v *LuckyFields) error {
if string(b) == "null" {
return nil
}
var tn struct {
TypeName string `json:"__typename"`
}
err := json.Unmarshal(m, &tn)
err := json.Unmarshal(b, &tn)
if err != nil {
return err
}
@@ -178,7 +188,7 @@ func __unmarshalLuckyFields(v *LuckyFields, m json.RawMessage) error {
switch tn.TypeName {
case "User":
*v = new(LuckyFieldsUser)
return json.Unmarshal(m, *v)
return json.Unmarshal(b, *v)
case "":
return fmt.Errorf(
"Response was missing Lucky.__typename")
@@ -196,6 +206,10 @@ type LuckyFieldsUser struct {
func (v *LuckyFieldsUser) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*LuckyFieldsUser
graphql.NoUnmarshalJSON
@@ -242,6 +256,10 @@ type UserFields struct {
func (v *UserFields) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*UserFields
graphql.NoUnmarshalJSON
@@ -266,6 +284,72 @@ func (v *UserFields) UnmarshalJSON(b []byte) error {
return nil
}
// __queryWithCustomMarshalInput is used internally by genqlient
type __queryWithCustomMarshalInput struct {
Date time.Time `json:"-"`
}
func (v *__queryWithCustomMarshalInput) MarshalJSON() ([]byte, error) {
var fullObject struct {
*__queryWithCustomMarshalInput
Date json.RawMessage `json:"date"`
graphql.NoMarshalJSON
}
fullObject.__queryWithCustomMarshalInput = v
{
dst := &fullObject.Date
src := v.Date
var err error
*dst, err = testutil.MarshalDate(
&src)
if err != nil {
return nil, fmt.Errorf(
"Unable to marshal __queryWithCustomMarshalInput.Date: %w", err)
}
}
return json.Marshal(&fullObject)
}
// __queryWithCustomMarshalSliceInput is used internally by genqlient
type __queryWithCustomMarshalSliceInput struct {
Dates []time.Time `json:"-"`
}
func (v *__queryWithCustomMarshalSliceInput) MarshalJSON() ([]byte, error) {
var fullObject struct {
*__queryWithCustomMarshalSliceInput
Dates []json.RawMessage `json:"dates"`
graphql.NoMarshalJSON
}
fullObject.__queryWithCustomMarshalSliceInput = v
{
dst := &fullObject.Dates
src := v.Dates
*dst = make(
[]json.RawMessage,
len(src))
for i, src := range src {
dst := &(*dst)[i]
var err error
*dst, err = testutil.MarshalDate(
&src)
if err != nil {
return nil, fmt.Errorf(
"Unable to marshal __queryWithCustomMarshalSliceInput.Dates: %w", err)
}
}
}
return json.Marshal(&fullObject)
}
// __queryWithFragmentsInput is used internally by genqlient
type __queryWithFragmentsInput struct {
Ids []string `json:"ids"`
@@ -312,6 +396,92 @@ type failingQueryResponse struct {
Me failingQueryMeUser `json:"me"`
}
// queryWithCustomMarshalResponse is returned by queryWithCustomMarshal on success.
type queryWithCustomMarshalResponse struct {
UsersBornOn []queryWithCustomMarshalUsersBornOnUser `json:"usersBornOn"`
}
// queryWithCustomMarshalSliceResponse is returned by queryWithCustomMarshalSlice on success.
type queryWithCustomMarshalSliceResponse struct {
UsersBornOnDates []queryWithCustomMarshalSliceUsersBornOnDatesUser `json:"usersBornOnDates"`
}
// queryWithCustomMarshalSliceUsersBornOnDatesUser includes the requested fields of the GraphQL type User.
type queryWithCustomMarshalSliceUsersBornOnDatesUser struct {
Id string `json:"id"`
Name string `json:"name"`
Birthdate time.Time `json:"-"`
}
func (v *queryWithCustomMarshalSliceUsersBornOnDatesUser) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*queryWithCustomMarshalSliceUsersBornOnDatesUser
Birthdate json.RawMessage `json:"birthdate"`
graphql.NoUnmarshalJSON
}
firstPass.queryWithCustomMarshalSliceUsersBornOnDatesUser = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
return err
}
{
dst := &v.Birthdate
src := firstPass.Birthdate
err = testutil.UnmarshalDate(
src, dst)
if err != nil {
return fmt.Errorf(
"Unable to unmarshal queryWithCustomMarshalSliceUsersBornOnDatesUser.Birthdate: %w", err)
}
}
return nil
}
// queryWithCustomMarshalUsersBornOnUser includes the requested fields of the GraphQL type User.
type queryWithCustomMarshalUsersBornOnUser struct {
Id string `json:"id"`
Name string `json:"name"`
Birthdate time.Time `json:"-"`
}
func (v *queryWithCustomMarshalUsersBornOnUser) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*queryWithCustomMarshalUsersBornOnUser
Birthdate json.RawMessage `json:"birthdate"`
graphql.NoUnmarshalJSON
}
firstPass.queryWithCustomMarshalUsersBornOnUser = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
return err
}
{
dst := &v.Birthdate
src := firstPass.Birthdate
err = testutil.UnmarshalDate(
src, dst)
if err != nil {
return fmt.Errorf(
"Unable to unmarshal queryWithCustomMarshalUsersBornOnUser.Birthdate: %w", err)
}
}
return nil
}
// queryWithFragmentsBeingsAnimal includes the requested fields of the GraphQL type Animal.
type queryWithFragmentsBeingsAnimal struct {
Typename string `json:"__typename"`
@@ -324,6 +494,10 @@ type queryWithFragmentsBeingsAnimal struct {
func (v *queryWithFragmentsBeingsAnimal) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*queryWithFragmentsBeingsAnimal
Owner json.RawMessage `json:"owner"`
@@ -337,10 +511,10 @@ func (v *queryWithFragmentsBeingsAnimal) UnmarshalJSON(b []byte) error {
}
{
target := &v.Owner
raw := firstPass.Owner
dst := &v.Owner
src := firstPass.Owner
err = __unmarshalqueryWithFragmentsBeingsAnimalOwnerBeing(
target, raw)
src, dst)
if err != nil {
return fmt.Errorf(
"Unable to unmarshal queryWithFragmentsBeingsAnimal.Owner: %w", err)
@@ -400,15 +574,15 @@ func (v *queryWithFragmentsBeingsAnimalOwnerAnimal) GetId() string { return v.Id
// GetName is a part of, and documented with, the interface queryWithFragmentsBeingsAnimalOwnerBeing.
func (v *queryWithFragmentsBeingsAnimalOwnerAnimal) GetName() string { return v.Name }
func __unmarshalqueryWithFragmentsBeingsAnimalOwnerBeing(v *queryWithFragmentsBeingsAnimalOwnerBeing, m json.RawMessage) error {
if string(m) == "null" {
func __unmarshalqueryWithFragmentsBeingsAnimalOwnerBeing(b []byte, v *queryWithFragmentsBeingsAnimalOwnerBeing) error {
if string(b) == "null" {
return nil
}
var tn struct {
TypeName string `json:"__typename"`
}
err := json.Unmarshal(m, &tn)
err := json.Unmarshal(b, &tn)
if err != nil {
return err
}
@@ -416,10 +590,10 @@ func __unmarshalqueryWithFragmentsBeingsAnimalOwnerBeing(v *queryWithFragmentsBe
switch tn.TypeName {
case "User":
*v = new(queryWithFragmentsBeingsAnimalOwnerUser)
return json.Unmarshal(m, *v)
return json.Unmarshal(b, *v)
case "Animal":
*v = new(queryWithFragmentsBeingsAnimalOwnerAnimal)
return json.Unmarshal(m, *v)
return json.Unmarshal(b, *v)
case "":
return fmt.Errorf(
"Response was missing Being.__typename")
@@ -474,15 +648,15 @@ func (v *queryWithFragmentsBeingsAnimal) GetId() string { return v.Id }
// GetName is a part of, and documented with, the interface queryWithFragmentsBeingsBeing.
func (v *queryWithFragmentsBeingsAnimal) GetName() string { return v.Name }
func __unmarshalqueryWithFragmentsBeingsBeing(v *queryWithFragmentsBeingsBeing, m json.RawMessage) error {
if string(m) == "null" {
func __unmarshalqueryWithFragmentsBeingsBeing(b []byte, v *queryWithFragmentsBeingsBeing) error {
if string(b) == "null" {
return nil
}
var tn struct {
TypeName string `json:"__typename"`
}
err := json.Unmarshal(m, &tn)
err := json.Unmarshal(b, &tn)
if err != nil {
return err
}
@@ -490,10 +664,10 @@ func __unmarshalqueryWithFragmentsBeingsBeing(v *queryWithFragmentsBeingsBeing,
switch tn.TypeName {
case "User":
*v = new(queryWithFragmentsBeingsUser)
return json.Unmarshal(m, *v)
return json.Unmarshal(b, *v)
case "Animal":
*v = new(queryWithFragmentsBeingsAnimal)
return json.Unmarshal(m, *v)
return json.Unmarshal(b, *v)
case "":
return fmt.Errorf(
"Response was missing Being.__typename")
@@ -524,6 +698,10 @@ type queryWithFragmentsResponse struct {
func (v *queryWithFragmentsResponse) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*queryWithFragmentsResponse
Beings []json.RawMessage `json:"beings"`
@@ -537,15 +715,15 @@ func (v *queryWithFragmentsResponse) UnmarshalJSON(b []byte) error {
}
{
target := &v.Beings
raw := firstPass.Beings
*target = make(
dst := &v.Beings
src := firstPass.Beings
*dst = make(
[]queryWithFragmentsBeingsBeing,
len(raw))
for i, raw := range raw {
target := &(*target)[i]
len(src))
for i, src := range src {
dst := &(*dst)[i]
err = __unmarshalqueryWithFragmentsBeingsBeing(
target, raw)
src, dst)
if err != nil {
return fmt.Errorf(
"Unable to unmarshal queryWithFragmentsResponse.Beings: %w", err)
@@ -601,15 +779,15 @@ func (v *queryWithInterfaceListFieldBeingsAnimal) GetId() string { return v.Id }
// GetName is a part of, and documented with, the interface queryWithInterfaceListFieldBeingsBeing.
func (v *queryWithInterfaceListFieldBeingsAnimal) GetName() string { return v.Name }
func __unmarshalqueryWithInterfaceListFieldBeingsBeing(v *queryWithInterfaceListFieldBeingsBeing, m json.RawMessage) error {
if string(m) == "null" {
func __unmarshalqueryWithInterfaceListFieldBeingsBeing(b []byte, v *queryWithInterfaceListFieldBeingsBeing) error {
if string(b) == "null" {
return nil
}
var tn struct {
TypeName string `json:"__typename"`
}
err := json.Unmarshal(m, &tn)
err := json.Unmarshal(b, &tn)
if err != nil {
return err
}
@@ -617,10 +795,10 @@ func __unmarshalqueryWithInterfaceListFieldBeingsBeing(v *queryWithInterfaceList
switch tn.TypeName {
case "User":
*v = new(queryWithInterfaceListFieldBeingsUser)
return json.Unmarshal(m, *v)
return json.Unmarshal(b, *v)
case "Animal":
*v = new(queryWithInterfaceListFieldBeingsAnimal)
return json.Unmarshal(m, *v)
return json.Unmarshal(b, *v)
case "":
return fmt.Errorf(
"Response was missing Being.__typename")
@@ -644,6 +822,10 @@ type queryWithInterfaceListFieldResponse struct {
func (v *queryWithInterfaceListFieldResponse) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*queryWithInterfaceListFieldResponse
Beings []json.RawMessage `json:"beings"`
@@ -657,15 +839,15 @@ func (v *queryWithInterfaceListFieldResponse) UnmarshalJSON(b []byte) error {
}
{
target := &v.Beings
raw := firstPass.Beings
*target = make(
dst := &v.Beings
src := firstPass.Beings
*dst = make(
[]queryWithInterfaceListFieldBeingsBeing,
len(raw))
for i, raw := range raw {
target := &(*target)[i]
len(src))
for i, src := range src {
dst := &(*dst)[i]
err = __unmarshalqueryWithInterfaceListFieldBeingsBeing(
target, raw)
src, dst)
if err != nil {
return fmt.Errorf(
"Unable to unmarshal queryWithInterfaceListFieldResponse.Beings: %w", err)
@@ -721,15 +903,15 @@ func (v *queryWithInterfaceListPointerFieldBeingsAnimal) GetId() string { return
// GetName is a part of, and documented with, the interface queryWithInterfaceListPointerFieldBeingsBeing.
func (v *queryWithInterfaceListPointerFieldBeingsAnimal) GetName() string { return v.Name }
func __unmarshalqueryWithInterfaceListPointerFieldBeingsBeing(v *queryWithInterfaceListPointerFieldBeingsBeing, m json.RawMessage) error {
if string(m) == "null" {
func __unmarshalqueryWithInterfaceListPointerFieldBeingsBeing(b []byte, v *queryWithInterfaceListPointerFieldBeingsBeing) error {
if string(b) == "null" {
return nil
}
var tn struct {
TypeName string `json:"__typename"`
}
err := json.Unmarshal(m, &tn)
err := json.Unmarshal(b, &tn)
if err != nil {
return err
}
@@ -737,10 +919,10 @@ func __unmarshalqueryWithInterfaceListPointerFieldBeingsBeing(v *queryWithInterf
switch tn.TypeName {
case "User":
*v = new(queryWithInterfaceListPointerFieldBeingsUser)
return json.Unmarshal(m, *v)
return json.Unmarshal(b, *v)
case "Animal":
*v = new(queryWithInterfaceListPointerFieldBeingsAnimal)
return json.Unmarshal(m, *v)
return json.Unmarshal(b, *v)
case "":
return fmt.Errorf(
"Response was missing Being.__typename")
@@ -764,6 +946,10 @@ type queryWithInterfaceListPointerFieldResponse struct {
func (v *queryWithInterfaceListPointerFieldResponse) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*queryWithInterfaceListPointerFieldResponse
Beings []json.RawMessage `json:"beings"`
@@ -777,16 +963,16 @@ func (v *queryWithInterfaceListPointerFieldResponse) UnmarshalJSON(b []byte) err
}
{
target := &v.Beings
raw := firstPass.Beings
*target = make(
dst := &v.Beings
src := firstPass.Beings
*dst = make(
[]*queryWithInterfaceListPointerFieldBeingsBeing,
len(raw))
for i, raw := range raw {
target := &(*target)[i]
*target = new(queryWithInterfaceListPointerFieldBeingsBeing)
len(src))
for i, src := range src {
dst := &(*dst)[i]
*dst = new(queryWithInterfaceListPointerFieldBeingsBeing)
err = __unmarshalqueryWithInterfaceListPointerFieldBeingsBeing(
*target, raw)
src, *dst)
if err != nil {
return fmt.Errorf(
"Unable to unmarshal queryWithInterfaceListPointerFieldResponse.Beings: %w", err)
@@ -835,15 +1021,15 @@ func (v *queryWithInterfaceNoFragmentsBeingAnimal) GetId() string { return v.Id
// GetName is a part of, and documented with, the interface queryWithInterfaceNoFragmentsBeing.
func (v *queryWithInterfaceNoFragmentsBeingAnimal) GetName() string { return v.Name }
func __unmarshalqueryWithInterfaceNoFragmentsBeing(v *queryWithInterfaceNoFragmentsBeing, m json.RawMessage) error {
if string(m) == "null" {
func __unmarshalqueryWithInterfaceNoFragmentsBeing(b []byte, v *queryWithInterfaceNoFragmentsBeing) error {
if string(b) == "null" {
return nil
}
var tn struct {
TypeName string `json:"__typename"`
}
err := json.Unmarshal(m, &tn)
err := json.Unmarshal(b, &tn)
if err != nil {
return err
}
@@ -851,10 +1037,10 @@ func __unmarshalqueryWithInterfaceNoFragmentsBeing(v *queryWithInterfaceNoFragme
switch tn.TypeName {
case "User":
*v = new(queryWithInterfaceNoFragmentsBeingUser)
return json.Unmarshal(m, *v)
return json.Unmarshal(b, *v)
case "Animal":
*v = new(queryWithInterfaceNoFragmentsBeingAnimal)
return json.Unmarshal(m, *v)
return json.Unmarshal(b, *v)
case "":
return fmt.Errorf(
"Response was missing Being.__typename")
@@ -892,6 +1078,10 @@ type queryWithInterfaceNoFragmentsResponse struct {
func (v *queryWithInterfaceNoFragmentsResponse) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*queryWithInterfaceNoFragmentsResponse
Being json.RawMessage `json:"being"`
@@ -905,10 +1095,10 @@ func (v *queryWithInterfaceNoFragmentsResponse) UnmarshalJSON(b []byte) error {
}
{
target := &v.Being
raw := firstPass.Being
dst := &v.Being
src := firstPass.Being
err = __unmarshalqueryWithInterfaceNoFragmentsBeing(
target, raw)
src, dst)
if err != nil {
return fmt.Errorf(
"Unable to unmarshal queryWithInterfaceNoFragmentsResponse.Being: %w", err)
@@ -926,6 +1116,10 @@ type queryWithNamedFragmentsBeingsAnimal struct {
func (v *queryWithNamedFragmentsBeingsAnimal) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*queryWithNamedFragmentsBeingsAnimal
graphql.NoUnmarshalJSON
@@ -976,15 +1170,15 @@ func (v *queryWithNamedFragmentsBeingsAnimal) GetTypename() string { return v.Ty
// GetId is a part of, and documented with, the interface queryWithNamedFragmentsBeingsBeing.
func (v *queryWithNamedFragmentsBeingsAnimal) GetId() string { return v.Id }
func __unmarshalqueryWithNamedFragmentsBeingsBeing(v *queryWithNamedFragmentsBeingsBeing, m json.RawMessage) error {
if string(m) == "null" {
func __unmarshalqueryWithNamedFragmentsBeingsBeing(b []byte, v *queryWithNamedFragmentsBeingsBeing) error {
if string(b) == "null" {
return nil
}
var tn struct {
TypeName string `json:"__typename"`
}
err := json.Unmarshal(m, &tn)
err := json.Unmarshal(b, &tn)
if err != nil {
return err
}
@@ -992,10 +1186,10 @@ func __unmarshalqueryWithNamedFragmentsBeingsBeing(v *queryWithNamedFragmentsBei
switch tn.TypeName {
case "User":
*v = new(queryWithNamedFragmentsBeingsUser)
return json.Unmarshal(m, *v)
return json.Unmarshal(b, *v)
case "Animal":
*v = new(queryWithNamedFragmentsBeingsAnimal)
return json.Unmarshal(m, *v)
return json.Unmarshal(b, *v)
case "":
return fmt.Errorf(
"Response was missing Being.__typename")
@@ -1014,6 +1208,10 @@ type queryWithNamedFragmentsBeingsUser struct {
func (v *queryWithNamedFragmentsBeingsUser) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*queryWithNamedFragmentsBeingsUser
graphql.NoUnmarshalJSON
@@ -1040,6 +1238,10 @@ type queryWithNamedFragmentsResponse struct {
func (v *queryWithNamedFragmentsResponse) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*queryWithNamedFragmentsResponse
Beings []json.RawMessage `json:"beings"`
@@ -1053,15 +1255,15 @@ func (v *queryWithNamedFragmentsResponse) UnmarshalJSON(b []byte) error {
}
{
target := &v.Beings
raw := firstPass.Beings
*target = make(
dst := &v.Beings
src := firstPass.Beings
*dst = make(
[]queryWithNamedFragmentsBeingsBeing,
len(raw))
for i, raw := range raw {
target := &(*target)[i]
len(src))
for i, src := range src {
dst := &(*dst)[i]
err = __unmarshalqueryWithNamedFragmentsBeingsBeing(
target, raw)
src, dst)
if err != nil {
return fmt.Errorf(
"Unable to unmarshal queryWithNamedFragmentsResponse.Beings: %w", err)
@@ -1214,6 +1416,64 @@ query queryWithOmitempty ($id: ID) {
return &retval, err
}
func queryWithCustomMarshal(
ctx context.Context,
client graphql.Client,
date time.Time,
) (*queryWithCustomMarshalResponse, error) {
__input := __queryWithCustomMarshalInput{
Date: date,
}
var err error
var retval queryWithCustomMarshalResponse
err = client.MakeRequest(
ctx,
"queryWithCustomMarshal",
`
query queryWithCustomMarshal ($date: Date!) {
usersBornOn(date: $date) {
id
name
birthdate
}
}
`,
&retval,
&__input,
)
return &retval, err
}
func queryWithCustomMarshalSlice(
ctx context.Context,
client graphql.Client,
dates []time.Time,
) (*queryWithCustomMarshalSliceResponse, error) {
__input := __queryWithCustomMarshalSliceInput{
Dates: dates,
}
var err error
var retval queryWithCustomMarshalSliceResponse
err = client.MakeRequest(
ctx,
"queryWithCustomMarshalSlice",
`
query queryWithCustomMarshalSlice ($dates: [Date!]!) {
usersBornOnDates(dates: $dates) {
id
name
birthdate
}
}
`,
&retval,
&__input,
)
return &retval, err
}
func queryWithInterfaceNoFragments(
ctx context.Context,
client graphql.Client,
+5
View File
@@ -3,3 +3,8 @@ operations:
- "*_test.go"
generated: generated.go
allow_broken_features: true
bindings:
Date:
type: time.Time
marshaler: "github.com/Khan/genqlient/internal/testutil.MarshalDate"
unmarshaler: "github.com/Khan/genqlient/internal/testutil.UnmarshalDate"
+59
View File
@@ -9,6 +9,7 @@ import (
"context"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -116,6 +117,64 @@ func TestOmitempty(t *testing.T) {
assert.Equal(t, 17, resp.User.LuckyNumber)
}
func TestCustomMarshal(t *testing.T) {
_ = `# @genqlient
query queryWithCustomMarshal($date: Date!) {
usersBornOn(date: $date) { id name birthdate }
}`
ctx := context.Background()
server := server.RunServer()
defer server.Close()
client := graphql.NewClient(server.URL, http.DefaultClient)
resp, err := queryWithCustomMarshal(ctx, client,
time.Date(2025, time.January, 1, 12, 34, 56, 789, time.UTC))
require.NoError(t, err)
assert.Len(t, resp.UsersBornOn, 1)
user := resp.UsersBornOn[0]
assert.Equal(t, "1", user.Id)
assert.Equal(t, "Yours Truly", user.Name)
assert.Equal(t,
time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC),
user.Birthdate)
resp, err = queryWithCustomMarshal(ctx, client,
time.Date(2021, time.January, 1, 12, 34, 56, 789, time.UTC))
require.NoError(t, err)
assert.Len(t, resp.UsersBornOn, 0)
}
func TestCustomMarshalSlice(t *testing.T) {
_ = `# @genqlient
query queryWithCustomMarshalSlice($dates: [Date!]!) {
usersBornOnDates(dates: $dates) { id name birthdate }
}`
ctx := context.Background()
server := server.RunServer()
defer server.Close()
client := graphql.NewClient(server.URL, http.DefaultClient)
resp, err := queryWithCustomMarshalSlice(ctx, client,
[]time.Time{time.Date(2025, time.January, 1, 12, 34, 56, 789, time.UTC)})
require.NoError(t, err)
assert.Len(t, resp.UsersBornOnDates, 1)
user := resp.UsersBornOnDates[0]
assert.Equal(t, "1", user.Id)
assert.Equal(t, "Yours Truly", user.Name)
assert.Equal(t,
time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC),
user.Birthdate)
resp, err = queryWithCustomMarshalSlice(ctx, client,
[]time.Time{time.Date(2021, time.January, 1, 12, 34, 56, 789, time.UTC)})
require.NoError(t, err)
assert.Len(t, resp.UsersBornOnDates, 0)
}
func TestInterfaceNoFragments(t *testing.T) {
_ = `# @genqlient
query queryWithInterfaceNoFragments($id: ID!) {
+5
View File
@@ -1,9 +1,13 @@
scalar Date
type Query {
me: User
user(id: ID): User
being(id: ID!): Being
beings(ids: [ID!]!): [Being]!
lotteryWinner(number: Int!): Lucky
usersBornOn(date: Date!): [User!]!
usersBornOnDates(dates: [Date!]!): [User!]!
fail: Boolean
}
@@ -12,6 +16,7 @@ type User implements Being & Lucky {
name: String!
luckyNumber: Int
hair: Hair
birthdate: Date
}
type Hair { color: String } # silly name to confuse the name-generator
+331 -7
View File
@@ -59,15 +59,18 @@ type ComplexityRoot struct {
}
Query struct {
Being func(childComplexity int, id string) int
Beings func(childComplexity int, ids []string) int
Fail func(childComplexity int) int
LotteryWinner func(childComplexity int, number int) int
Me func(childComplexity int) int
User func(childComplexity int, id *string) int
Being func(childComplexity int, id string) int
Beings func(childComplexity int, ids []string) int
Fail func(childComplexity int) int
LotteryWinner func(childComplexity int, number int) int
Me func(childComplexity int) int
User func(childComplexity int, id *string) int
UsersBornOn func(childComplexity int, date string) int
UsersBornOnDates func(childComplexity int, dates []string) int
}
User struct {
Birthdate func(childComplexity int) int
Hair func(childComplexity int) int
ID func(childComplexity int) int
LuckyNumber func(childComplexity int) int
@@ -81,6 +84,8 @@ type QueryResolver interface {
Being(ctx context.Context, id string) (Being, error)
Beings(ctx context.Context, ids []string) ([]Being, error)
LotteryWinner(ctx context.Context, number int) (Lucky, error)
UsersBornOn(ctx context.Context, date string) ([]*User, error)
UsersBornOnDates(ctx context.Context, dates []string) ([]*User, error)
Fail(ctx context.Context) (*bool, error)
}
@@ -210,6 +215,37 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Query.User(childComplexity, args["id"].(*string)), true
case "Query.usersBornOn":
if e.complexity.Query.UsersBornOn == nil {
break
}
args, err := ec.field_Query_usersBornOn_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Query.UsersBornOn(childComplexity, args["date"].(string)), true
case "Query.usersBornOnDates":
if e.complexity.Query.UsersBornOnDates == nil {
break
}
args, err := ec.field_Query_usersBornOnDates_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Query.UsersBornOnDates(childComplexity, args["dates"].([]string)), true
case "User.birthdate":
if e.complexity.User.Birthdate == nil {
break
}
return e.complexity.User.Birthdate(childComplexity), true
case "User.hair":
if e.complexity.User.Hair == nil {
break
@@ -288,12 +324,16 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er
}
var sources = []*ast.Source{
{Name: "../schema.graphql", Input: `type Query {
{Name: "../schema.graphql", Input: `scalar Date
type Query {
me: User
user(id: ID): User
being(id: ID!): Being
beings(ids: [ID!]!): [Being]!
lotteryWinner(number: Int!): Lucky
usersBornOn(date: Date!): [User!]!
usersBornOnDates(dates: [Date!]!): [User!]!
fail: Boolean
}
@@ -302,6 +342,7 @@ type User implements Being & Lucky {
name: String!
luckyNumber: Int
hair: Hair
birthdate: Date
}
type Hair { color: String } # silly name to confuse the name-generator
@@ -412,6 +453,36 @@ func (ec *executionContext) field_Query_user_args(ctx context.Context, rawArgs m
return args, nil
}
func (ec *executionContext) field_Query_usersBornOnDates_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 []string
if tmp, ok := rawArgs["dates"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("dates"))
arg0, err = ec.unmarshalNDate2ᚕstringᚄ(ctx, tmp)
if err != nil {
return nil, err
}
}
args["dates"] = arg0
return args, nil
}
func (ec *executionContext) field_Query_usersBornOn_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 string
if tmp, ok := rawArgs["date"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("date"))
arg0, err = ec.unmarshalNDate2string(ctx, tmp)
if err != nil {
return nil, err
}
}
args["date"] = arg0
return args, nil
}
func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@@ -877,6 +948,90 @@ func (ec *executionContext) _Query_lotteryWinner(ctx context.Context, field grap
return ec.marshalOLucky2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐLucky(ctx, field.Selections, res)
}
func (ec *executionContext) _Query_usersBornOn(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Query",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Query_usersBornOn_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().UsersBornOn(rctx, args["date"].(string))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.([]*User)
fc.Result = res
return ec.marshalNUser2ᚕᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐUserᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) _Query_usersBornOnDates(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Query",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Query_usersBornOnDates_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().UsersBornOnDates(rctx, args["dates"].([]string))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.([]*User)
fc.Result = res
return ec.marshalNUser2ᚕᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐUserᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) _Query_fail(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -1114,6 +1269,38 @@ func (ec *executionContext) _User_hair(ctx context.Context, field graphql.Collec
return ec.marshalOHair2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐHair(ctx, field.Selections, res)
}
func (ec *executionContext) _User_birthdate(ctx context.Context, field graphql.CollectedField, obj *User) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "User",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Birthdate, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalODate2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -2413,6 +2600,34 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
res = ec._Query_lotteryWinner(ctx, field)
return res
})
case "usersBornOn":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Query_usersBornOn(ctx, field)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "usersBornOnDates":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Query_usersBornOnDates(ctx, field)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "fail":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
@@ -2464,6 +2679,8 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj
out.Values[i] = ec._User_luckyNumber(ctx, field, obj)
case "hair":
out.Values[i] = ec._User_hair(ctx, field, obj)
case "birthdate":
out.Values[i] = ec._User_birthdate(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@@ -2772,6 +2989,51 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se
return res
}
func (ec *executionContext) unmarshalNDate2string(ctx context.Context, v interface{}) (string, error) {
res, err := graphql.UnmarshalString(v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNDate2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler {
res := graphql.MarshalString(v)
if res == graphql.Null {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
}
return res
}
func (ec *executionContext) unmarshalNDate2ᚕstringᚄ(ctx context.Context, v interface{}) ([]string, error) {
var vSlice []interface{}
if v != nil {
if tmp1, ok := v.([]interface{}); ok {
vSlice = tmp1
} else {
vSlice = []interface{}{v}
}
}
var err error
res := make([]string, len(vSlice))
for i := range vSlice {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i))
res[i], err = ec.unmarshalNDate2string(ctx, vSlice[i])
if err != nil {
return nil, err
}
}
return res, nil
}
func (ec *executionContext) marshalNDate2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler {
ret := make(graphql.Array, len(v))
for i := range v {
ret[i] = ec.marshalNDate2string(ctx, sel, v[i])
}
return ret
}
func (ec *executionContext) unmarshalNID2string(ctx context.Context, v interface{}) (string, error) {
res, err := graphql.UnmarshalID(v)
return res, graphql.ErrorOnPath(ctx, err)
@@ -2857,6 +3119,53 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S
return res
}
func (ec *executionContext) marshalNUser2ᚕᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐUserᚄ(ctx context.Context, sel ast.SelectionSet, v []*User) graphql.Marshaler {
ret := make(graphql.Array, len(v))
var wg sync.WaitGroup
isLen1 := len(v) == 1
if !isLen1 {
wg.Add(len(v))
}
for i := range v {
i := i
fc := &graphql.FieldContext{
Index: &i,
Result: &v[i],
}
ctx := graphql.WithFieldContext(ctx, fc)
f := func(i int) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = nil
}
}()
if !isLen1 {
defer wg.Done()
}
ret[i] = ec.marshalNUser2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐUser(ctx, sel, v[i])
}
if isLen1 {
f(i)
} else {
go f(i)
}
}
wg.Wait()
return ret
}
func (ec *executionContext) marshalNUser2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐUser(ctx context.Context, sel ast.SelectionSet, v *User) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._User(ctx, sel, v)
}
func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler {
return ec.___Directive(ctx, sel, &v)
}
@@ -3124,6 +3433,21 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast
return graphql.MarshalBoolean(*v)
}
func (ec *executionContext) unmarshalODate2ᚖstring(ctx context.Context, v interface{}) (*string, error) {
if v == nil {
return nil, nil
}
res, err := graphql.UnmarshalString(v)
return &res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalODate2ᚖstring(ctx context.Context, sel ast.SelectionSet, v *string) graphql.Marshaler {
if v == nil {
return graphql.Null
}
return graphql.MarshalString(*v)
}
func (ec *executionContext) marshalOHair2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐHair(ctx context.Context, sel ast.SelectionSet, v *Hair) graphql.Marshaler {
if v == nil {
return graphql.Null
+5 -4
View File
@@ -35,10 +35,11 @@ type Hair struct {
}
type User struct {
ID string `json:"id"`
Name string `json:"name"`
LuckyNumber *int `json:"luckyNumber"`
Hair *Hair `json:"hair"`
ID string `json:"id"`
Name string `json:"name"`
LuckyNumber *int `json:"luckyNumber"`
Hair *Hair `json:"hair"`
Birthdate *string `json:"birthdate"`
}
func (User) IsBeing() {}
+22 -1
View File
@@ -15,7 +15,8 @@ func intptr(v int) *int { return &v }
var users = []*User{
{
ID: "1", Name: "Yours Truly", LuckyNumber: intptr(17),
Hair: &Hair{Color: strptr("Black")},
Birthdate: strptr("2025-01-01"),
Hair: &Hair{Color: strptr("Black")},
},
{ID: "2", Name: "Raven", LuckyNumber: intptr(-1), Hair: nil},
}
@@ -40,6 +41,18 @@ func userByID(id string) *User {
return nil
}
func usersByBirthdates(dates []string) []*User {
var retval []*User
for _, date := range dates {
for _, user := range users {
if user.Birthdate != nil && *user.Birthdate == date {
retval = append(retval, user)
}
}
}
return retval
}
func beingByID(id string) Being {
for _, user := range users {
if id == user.ID {
@@ -86,6 +99,14 @@ func (r *queryResolver) LotteryWinner(ctx context.Context, number int) (Lucky, e
return nil, nil
}
func (r *queryResolver) UsersBornOn(ctx context.Context, date string) ([]*User, error) {
return usersByBirthdates([]string{date}), nil
}
func (r *queryResolver) UsersBornOnDates(ctx context.Context, dates []string) ([]*User, error) {
return usersByBirthdates(dates), nil
}
func (r *queryResolver) Fail(ctx context.Context) (*bool, error) {
f := true
return &f, fmt.Errorf("oh no")
+18
View File
@@ -2,6 +2,7 @@ package testutil
import (
"context"
"time"
"github.com/Khan/genqlient/graphql"
)
@@ -26,3 +27,20 @@ type MyContext interface {
func GetClientFromNowhere() (graphql.Client, error) { return nil, nil }
func GetClientFromContext(ctx context.Context) (graphql.Client, error) { return nil, nil }
func GetClientFromMyContext(ctx MyContext) (graphql.Client, error) { return nil, nil }
const dateFormat = "2006-01-02"
func MarshalDate(t *time.Time) ([]byte, error) {
return []byte(`"` + t.Format(dateFormat) + `"`), nil
}
func UnmarshalDate(b []byte, t *time.Time) error {
// (modified from time.Time.UnmarshalJSON)
if string(b) == "null" {
return nil
}
var err error
*t, err = time.Parse(`"`+dateFormat+`"`, string(b))
return err
}