-
Notifications
You must be signed in to change notification settings - Fork 114
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
If requested, validate binding-types get the right fields (#70)
## Summary: One sharp edge of the new `bindings` setting (when used for composite types) is this: the (presumably struct) type to which you're binding may expect to have particular fields, but it's GraphQL so you could have requested some other set of fields. Now, if you ask us, we check. Specifically, I've added a new setting under the `bindings` items, which says: everywhere we query this must select these fields. (Or use its own inline `# @genqlient(bind: ...)`.) It must select exactly those fields, in order, no more, no less. This was fairly easy to implement; actually comparing the selections was surprisingly much code but it's all pretty straightforward. ## Test plan: make check Author: benjaminjkraft Reviewers: dnerdy, aberkan, csilvers, MiguelCastillo Required Reviewers: Approved by: dnerdy 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: #70
- Loading branch information
1 parent
fc4aa08
commit 95609f7
Showing
10 changed files
with
180 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
package generate | ||
|
||
// This file is responsible for doing the validation for type-bindings, if they | ||
// are so configured (see TypeBinding). | ||
|
||
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. | ||
// | ||
// TODO(benkraft): Should we check arguments/directives? | ||
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package errors | ||
|
||
const _ = `# @genqlient | ||
query GetPokemonWrongFields { | ||
pokemon { species } | ||
} | ||
` |
3 changes: 3 additions & 0 deletions
3
generate/testdata/errors/BindingWithIncorrectSelection.graphql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
query GetPokemonWrongFields { | ||
pokemon { species species } | ||
} |
8 changes: 8 additions & 0 deletions
8
generate/testdata/errors/BindingWithIncorrectSelection.schema.graphql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
type Query { | ||
pokemon: Pokemon | ||
} | ||
|
||
type Pokemon { | ||
species: String! | ||
level: Int! | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
generate/testdata/snapshots/TestGenerateErrors-BindingWithIncorrectSelection.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
invalid selection for type-binding GetPokemonWrongFieldsPokemon: testdata/errors/BindingWithIncorrectSelection.schema.graphql:2: expected 2 fields, got 1 |
1 change: 1 addition & 0 deletions
1
generate/testdata/snapshots/TestGenerateErrors-BindingWithIncorrectSelection.graphql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
invalid selection for type-binding GetPokemonWrongFieldsPokemon: testdata/errors/BindingWithIncorrectSelection.graphql:2: expected field 1 to be level, got species |