Files
Ben Kraft fcae8dd1d7 Add support for specifying type-names, and conflict-detection (#94)
## Summary:
In this commit I add two related features to genqlient:
conflict-detection to avoid generating two distinct types with the same
name, and an option to specify the type-name genqlient should use for
some type.

The conflict-detection was pretty simple once I realized I had already
written all the code to do it in #70.  There was a bunch of wiring,
since we now need to keep track of the GraphQL type/selection-set that
each type corresponds to, but it was pretty straightforward.  This
allows us to:
- detect and reject if you have really sneaky type-names (there are some
  examples documented in `names.go`)
- more clearly crash if genqlient accidentally generates two conflicting
  types, and
- avoid stack-overflow when handing recursive (input) types (although
  sadly the poor support for options on input types (#14) makes them
  difficult to use in many cases; you really need to be able to set
  `pointer: true`)

And with that all set up, the type-naming was also easy!  (It doesn't
have to get into the core of the type-generator, just plug in where we
choose names.  The desire for conflict detection was the main reason I
hadn't set it up already.)  Note that the existing limitation of #70 that
the fields have to be in exactly the same order remains (and is now
documented as #93); it's not deeply hard to fix but it's surprisingly
much work.

Issue: https://github.com/Khan/genqlient/issues/60
Issue: https://github.com/Khan/genqlient/issues/12

## Test plan:
make check


Author: benjaminjkraft

Reviewers: StevenACoffman, jvoll, benjaminjkraft, aberkan, csilvers, dnerdy, mahtabsabet, MiguelCastillo

Required Reviewers: 

Approved By: StevenACoffman, jvoll

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/94
2021-09-15 18:06:43 -07:00

113 lines
3.6 KiB
Go

package generate
// This file contains helpers to do various bits of validation in the process
// of converting types to Go, notably, for cases where we need to check that
// two types match.
import (
"fmt"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/parser"
)
// selectionsMatch recursively compares the two selection-sets, and returns an
// error if they differ.
//
// It does not check arguments and directives, only field names, aliases,
// order, and fragment-structure. It does not recurse into named fragments, it
// only checks that their names match.
//
// If both selection-sets are nil/empty, they compare equal.
func selectionsMatch(
pos *ast.Position,
expectedSelectionSet, actualSelectionSet ast.SelectionSet,
) error {
if len(expectedSelectionSet) != len(actualSelectionSet) {
return errorf(
pos, "expected %d fields, got %d",
len(expectedSelectionSet), len(actualSelectionSet))
}
for i, expected := range expectedSelectionSet {
switch expected := expected.(type) {
case *ast.Field:
actual, ok := actualSelectionSet[i].(*ast.Field)
switch {
case !ok:
return errorf(actual.Position,
"expected selection #%d to be field, got %T",
i, actualSelectionSet[i])
case actual.Name != expected.Name:
return errorf(actual.Position,
"expected field %d to be %s, got %s",
i, expected.Name, actual.Name)
case actual.Alias != expected.Alias:
return errorf(actual.Position,
"expected field %d's alias to be %s, got %s",
i, expected.Alias, actual.Alias)
}
err := selectionsMatch(actual.Position, expected.SelectionSet, actual.SelectionSet)
if err != nil {
return fmt.Errorf("in %s sub-selection: %w", actual.Alias, err)
}
case *ast.InlineFragment:
actual, ok := actualSelectionSet[i].(*ast.InlineFragment)
switch {
case !ok:
return errorf(actual.Position,
"expected selection %d to be inline fragment, got %T",
i, actualSelectionSet[i])
case actual.TypeCondition != expected.TypeCondition:
return errorf(actual.Position,
"expected fragment %d to be on type %s, got %s",
i, expected.TypeCondition, actual.TypeCondition)
}
err := selectionsMatch(actual.Position, expected.SelectionSet, actual.SelectionSet)
if err != nil {
return fmt.Errorf("in inline fragment on %s: %w", actual.TypeCondition, err)
}
case *ast.FragmentSpread:
actual, ok := actualSelectionSet[i].(*ast.FragmentSpread)
switch {
case !ok:
return errorf(actual.Position,
"expected selection %d to be fragment spread, got %T",
i, actualSelectionSet[i])
case actual.Name != expected.Name:
return errorf(actual.Position,
"expected fragment %d to be ...%s, got ...%s",
i, expected.Name, actual.Name)
}
}
}
return nil
}
// validateBindingSelection checks that if you requested in your type-binding
// that this type must always request certain fields, then in fact it does.
func (g *generator) validateBindingSelection(
typeName string,
binding *TypeBinding,
pos *ast.Position,
selectionSet ast.SelectionSet,
) error {
if binding.ExpectExactFields == "" {
return nil // no validation requested
}
// HACK: we parse the selection as if it were a query, which is basically
// the same (for syntax purposes; it of course wouldn't validate)
doc, gqlErr := parser.ParseQuery(&ast.Source{Input: binding.ExpectExactFields})
if gqlErr != nil {
return errorf(
nil, "invalid type-binding %s.expect_exact_fields: %w", typeName, gqlErr)
}
err := selectionsMatch(pos, doc.Operations[0].SelectionSet, selectionSet)
if err != nil {
return fmt.Errorf("invalid selection for type-binding %s: %w", typeName, err)
}
return nil
}