Document and improve support for binding non-scalars to a specific type (#69)

## Summary:
We had this setting called "scalars", which said: bind this GraphQL type
to this Go type, rather than the one you would normally use.  It's
called that because it's most useful for custom scalars, where "the one
you would normally use" is "error: unknown scalar".  But nothing ever
stopped you from using it for a non-scalar type.  I was planning on
removing this functionality, because it's sort of a rough edge, but a
discussion with Craig found some good use cases, so instead, in this
commit, I document it better and add some slightly nicer ways to specify
it.

Specifically, here are a few potential non-scalar use cases:
- bind a GraphQL enum to a nonstandard type (or even `string`)
- bind an input type to some type that has exactly the fields you want;
  this acts as a sort of workaround for issues #14 and #44
- bind an object type to your own struct, so as to add methods to it
  (this is the use case Craig raised)
- bind an object type to your own struct, so as to share it between
  multiple queries (I believe named fragments will address this case
  better, but it doesn't hurt to have options)
- bind a GraphQL list type to a non-slice type in Go (presumably one
  with an UnmarshalJSON method), or any other different structure
The latter three cases still have the sharp edge I was originally
worried about, which is that nothing guarantees that the fields you
request in the query are the ones the type expects to get.  But I think
it's worth having the option, with appropriate disclaimers.

The main change to help support that better is that you can now specify
the type inline in the query, as an alternative to specifying it in the
config file; this means you might map a given object to a given struct,
but only in some cases, and when you do you have a chance to look at the
list of fields you're requesting.

Additionally, I renamed the config field from "scalars" to "bindings"
(but mentioned it in a few places where you might go looking for how to
map scalars, most importantly the error message you get for an unknown
(custom) scalar).  While I was making a breaking change, I also changed
it to be a `map[string]<struct>` instead of a `map[string]string`,
because I expect to add more fields soon, e.g. to handle issue #38.

Finally, since the feature is now intended/documented, I added some
tests, although it's honestly quite simple on the genqlient side.

## Test plan:
make tesc


Author: benjaminjkraft

Reviewers: csilvers, aberkan, dnerdy, MiguelCastillo

Required Reviewers: 

Approved by: csilvers

Checks:  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Test (1.13),  Lint,  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Test (1.13),  Lint

Pull request URL: https://github.com/Khan/genqlient/pull/69
This commit is contained in:
Ben Kraft
2021-08-27 15:57:10 -07:00
committed by GitHub
parent bf266d874d
commit 222d6f191a
18 changed files with 258 additions and 50 deletions
+45 -4
View File
@@ -63,6 +63,27 @@ type GenqlientDirective struct {
// pointer to save copies) or if you wish to distinguish between the Go
// zero value and null (for nullable fields).
Pointer *bool
// If set, this argument or field will use the given Go type instead of a
// genqlient-generated type.
//
// The value should be the fully-qualified type name to use for the field,
// for example:
// time.Time
// map[string]interface{}
// []github.com/you/yourpkg/subpkg.MyType
// Note that the type is the type of the whole field, e.g. if your field in
// GraphQL has type `[DateTime]`, you'd do
// # @genqlient(bind: "[]time.Time")
// (But you're not required to; if you want to map to some type DateList,
// you can do that, as long as its UnmarshalJSON method can accept a list
// of datetimes.)
//
// See Config.Bindings for more details; this is effectively to a local
// version of that global setting and should be used with similar care.
// If set to "-", overrides any such global setting and uses a
// genqlient-generated type.
Bind string
}
func (dir *GenqlientDirective) GetOmitempty() bool { return dir.Omitempty != nil && *dir.Omitempty }
@@ -70,7 +91,6 @@ func (dir *GenqlientDirective) GetPointer() bool { return dir.Pointer != nil &
func setBool(dst **bool, v *ast.Value) error {
ei, err := v.Value(nil) // no vars allowed
// TODO: here and below, put positions on these errors
if err != nil {
return errorf(v.Position, "invalid boolean value %v: %v", v, err)
}
@@ -81,6 +101,18 @@ func setBool(dst **bool, v *ast.Value) error {
return errorf(v.Position, "expected boolean, got non-boolean value %T(%v)", ei, ei)
}
func setString(dst *string, v *ast.Value) error {
ei, err := v.Value(nil) // no vars allowed
if err != nil {
return errorf(v.Position, "invalid string value %v: %v", v, err)
}
if b, ok := ei.(string); ok {
*dst = b
return nil
}
return errorf(v.Position, "expected string, got non-string value %T(%v)", ei, ei)
}
func fromGraphQL(dir *ast.Directive) (*GenqlientDirective, error) {
if dir.Name != "genqlient" {
// Actually we just won't get here; we only get here if the line starts
@@ -99,6 +131,8 @@ func fromGraphQL(dir *ast.Directive) (*GenqlientDirective, error) {
err = setBool(&retval.Omitempty, arg.Value)
case "pointer":
err = setBool(&retval.Pointer, arg.Value)
case "bind":
err = setString(&retval.Bind, arg.Value)
default:
return nil, errorf(arg.Position, "unknown argument %v for @genqlient", arg.Name)
}
@@ -112,8 +146,12 @@ func fromGraphQL(dir *ast.Directive) (*GenqlientDirective, error) {
func (dir *GenqlientDirective) validate(node interface{}) error {
switch node := node.(type) {
case *ast.OperationDefinition:
// Anything is valid on the entire operation; it will just apply to
// whatever it is relevant to.
if dir.Bind != "" {
return errorf(dir.pos, "bind may not be applied to the entire operation")
}
// Anything else is valid on the entire operation; it will just apply
// to whatever it is relevant to.
return nil
case *ast.VariableDefinition:
if dir.Omitempty != nil && node.Type.NonNull {
@@ -122,7 +160,7 @@ func (dir *GenqlientDirective) validate(node interface{}) error {
return nil
case *ast.Field:
if dir.Omitempty != nil {
return errorf(dir.pos, "omitempty is not appilcable to fields")
return errorf(dir.pos, "omitempty is not applicable to fields")
}
return nil
default:
@@ -138,6 +176,9 @@ func (dir *GenqlientDirective) merge(other *GenqlientDirective) *GenqlientDirect
if other.Pointer != nil {
retval.Pointer = other.Pointer
}
if other.Bind != "" {
retval.Bind = other.Bind
}
return &retval
}