fcae8dd1d7
## 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
113 lines
3.6 KiB
Go
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
|
|
}
|