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
+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")