Add support for "flattening" fragment-spreads (#121)
## Summary:
One common use of fragment spreads is as the entirety of a field's
selection, e.g.
```graphql
query MyQuery {
myField {
...MyFragment
}
}
```
In this case, by default, genqlient generates a wrapper type
`MyQueryMyFieldMyType`, which just embeds `MyFragment`. This makes
sense if you later want to add more fields in addition to the fragment
spread. But if you don't -- and you did the fragment because you want
to share types, it's an extra layer of indirection. (Which becomes
especially onerous if `myField` has list type (`[MyType!]`), such that
it's not just an extra attribute-access to get to `MyFragment`.)
The new option `# @genqlient(flatten: true)` simplifies this situation:
if applied to `myField` is skips the wrapper type;
`MyQueryResponse.MyField` will simply have type `MyFragment` (or
`[]MyFragment`, or whatever). This should hopefully make the `typename`
option, which has more limitations, less necessary.
Note that in #30 the initial idea was to support this for fields as
well. This would require significant additional complexity in the
JSON-(un)marshaling code, and has proven less necessary, so I
implemented this option only for fragment-spreads for now. With that
restriction, it was shockingly simple; we have to hook into a bunch of
different places, but they're all quite simple, since the structure of
the Go types still matches the structure in GraphQL.
Issue: https://github.com/Khan/genqlient/issues/30
## Test plan:
make check
Author: benjaminjkraft
Reviewers: csilvers, dnerdy, aberkan, jvoll, mahtabsabet, MiguelCastillo, StevenACoffman
Required Reviewers:
Approved By: csilvers, 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/121
This commit is contained in:
@@ -15,6 +15,7 @@ type genqlientDirective struct {
|
||||
Omitempty *bool
|
||||
Pointer *bool
|
||||
Struct *bool
|
||||
Flatten *bool
|
||||
Bind string
|
||||
TypeName string
|
||||
}
|
||||
@@ -28,6 +29,7 @@ func newGenqlientDirective(pos *ast.Position) *genqlientDirective {
|
||||
func (dir *genqlientDirective) GetOmitempty() bool { return dir.Omitempty != nil && *dir.Omitempty }
|
||||
func (dir *genqlientDirective) GetPointer() bool { return dir.Pointer != nil && *dir.Pointer }
|
||||
func (dir *genqlientDirective) GetStruct() bool { return dir.Struct != nil && *dir.Struct }
|
||||
func (dir *genqlientDirective) GetFlatten() bool { return dir.Flatten != nil && *dir.Flatten }
|
||||
|
||||
func setBool(optionName string, dst **bool, v *ast.Value, pos *ast.Position) error {
|
||||
if *dst != nil {
|
||||
@@ -85,6 +87,8 @@ func (dir *genqlientDirective) add(graphQLDirective *ast.Directive, pos *ast.Pos
|
||||
err = setBool("pointer", &dir.Pointer, arg.Value, pos)
|
||||
case "struct":
|
||||
err = setBool("struct", &dir.Struct, arg.Value, pos)
|
||||
case "flatten":
|
||||
err = setBool("flatten", &dir.Flatten, arg.Value, pos)
|
||||
case "bind":
|
||||
err = setString("bind", &dir.Bind, arg.Value, pos)
|
||||
case "typename":
|
||||
@@ -116,7 +120,7 @@ func (dir *genqlientDirective) validate(node interface{}, schema *ast.Schema) er
|
||||
}
|
||||
|
||||
if dir.Struct != nil {
|
||||
return errorf(dir.pos, "struct is only applicable to fields")
|
||||
return errorf(dir.pos, "struct is only applicable to fields, not frragment-definitions")
|
||||
}
|
||||
|
||||
// Like operations, anything else will just apply to the entire
|
||||
@@ -128,22 +132,32 @@ func (dir *genqlientDirective) validate(node interface{}, schema *ast.Schema) er
|
||||
}
|
||||
|
||||
if dir.Struct != nil {
|
||||
return errorf(dir.pos, "struct is only applicable to fields")
|
||||
return errorf(dir.pos, "struct is only applicable to fields, not variable-definitions")
|
||||
}
|
||||
|
||||
if dir.Flatten != nil {
|
||||
return errorf(dir.pos, "flatten is only applicable to fields, not variable-definitions")
|
||||
}
|
||||
|
||||
return nil
|
||||
case *ast.Field:
|
||||
if dir.Omitempty != nil {
|
||||
return errorf(dir.pos, "omitempty is not applicable to fields")
|
||||
return errorf(dir.pos, "omitempty is not applicable to variables, not fields")
|
||||
}
|
||||
|
||||
typ := schema.Types[node.Definition.Type.Name()]
|
||||
if dir.Struct != nil {
|
||||
typ := schema.Types[node.Definition.Type.Name()]
|
||||
if err := validateStructOption(typ, node.SelectionSet, dir.pos); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if dir.Flatten != nil {
|
||||
if _, err := validateFlattenOption(typ, node.SelectionSet, dir.pos); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
default:
|
||||
return errorf(dir.pos, "invalid @genqlient directive location: %T", node)
|
||||
@@ -178,6 +192,58 @@ func validateStructOption(
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateFlattenOption(
|
||||
typ *ast.Definition,
|
||||
selectionSet ast.SelectionSet,
|
||||
pos *ast.Position,
|
||||
) (index int, err error) {
|
||||
index = -1
|
||||
if len(selectionSet) == 0 {
|
||||
return -1, errorf(pos, "flatten is not allowed for leaf fields")
|
||||
}
|
||||
|
||||
for i, selection := range selectionSet {
|
||||
switch selection := selection.(type) {
|
||||
case *ast.Field:
|
||||
// If the field is auto-added __typename, ignore it for flattening
|
||||
// purposes.
|
||||
if selection.Name == "__typename" && selection.Position == nil {
|
||||
continue
|
||||
}
|
||||
// Type-wise, it's no harder to implement flatten for fields, but
|
||||
// it requires new logic in UnmarshalJSON. We can add that if it
|
||||
// proves useful relative to its complexity.
|
||||
return -1, errorf(pos, "flatten is not yet supported for fields (only fragment spreads)")
|
||||
|
||||
case *ast.InlineFragment:
|
||||
// Inline fragments aren't allowed. In principle there's nothing
|
||||
// stopping us from allowing them (under the same type-match
|
||||
// conditions as fragment spreads), but there's little value to it.
|
||||
return -1, errorf(pos, "flatten is not allowed for selections with inline fragments")
|
||||
|
||||
case *ast.FragmentSpread:
|
||||
if index != -1 {
|
||||
return -1, errorf(pos, "flatten is not allowed for fields with multiple selections")
|
||||
} else if !fragmentMatches(typ, selection.Definition.Definition) {
|
||||
// We don't let you flatten
|
||||
// field { # type: FieldType
|
||||
// ...Fragment # type: FragmentType
|
||||
// }
|
||||
// unless FragmentType implements FieldType, because otherwise
|
||||
// what do we do if we get back a type that doesn't implement
|
||||
// FragmentType?
|
||||
return -1, errorf(pos,
|
||||
"flatten is not allowed for fields with fragment-spreads "+
|
||||
"unless the field-type implements the fragment-type; "+
|
||||
"field-type %s does not implement fragment-type %s",
|
||||
typ.Name, selection.Definition.Definition.Name)
|
||||
}
|
||||
index = i
|
||||
}
|
||||
}
|
||||
return index, nil
|
||||
}
|
||||
|
||||
// merge joins the directive applied to this node (the argument) and the one
|
||||
// applied to the entire operation (the receiver) and returns a new
|
||||
// directive-object representing the options to apply to this node (where in
|
||||
@@ -193,6 +259,9 @@ func (dir *genqlientDirective) merge(other *genqlientDirective) *genqlientDirect
|
||||
if other.Struct != nil {
|
||||
retval.Struct = other.Struct
|
||||
}
|
||||
if other.Flatten != nil {
|
||||
retval.Flatten = other.Flatten
|
||||
}
|
||||
if other.Bind != "" {
|
||||
retval.Bind = other.Bind
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user