github.com/nathanstitt/genqlient@v0.3.1-0.20211028004951-a2bda3c41ab8/generate/validation.go (about) 1 package generate 2 3 // This file contains helpers to do various bits of validation in the process 4 // of converting types to Go, notably, for cases where we need to check that 5 // two types match. 6 7 import ( 8 "fmt" 9 10 "github.com/vektah/gqlparser/v2/ast" 11 "github.com/vektah/gqlparser/v2/parser" 12 ) 13 14 // selectionsMatch recursively compares the two selection-sets, and returns an 15 // error if they differ. 16 // 17 // It does not check arguments and directives, only field names, aliases, 18 // order, and fragment-structure. It does not recurse into named fragments, it 19 // only checks that their names match. 20 // 21 // If both selection-sets are nil/empty, they compare equal. 22 func selectionsMatch( 23 pos *ast.Position, 24 expectedSelectionSet, actualSelectionSet ast.SelectionSet, 25 ) error { 26 if len(expectedSelectionSet) != len(actualSelectionSet) { 27 return errorf( 28 pos, "expected %d fields, got %d", 29 len(expectedSelectionSet), len(actualSelectionSet)) 30 } 31 32 for i, expected := range expectedSelectionSet { 33 switch expected := expected.(type) { 34 case *ast.Field: 35 actual, ok := actualSelectionSet[i].(*ast.Field) 36 switch { 37 case !ok: 38 return errorf(actual.Position, 39 "expected selection #%d to be field, got %T", 40 i, actualSelectionSet[i]) 41 case actual.Name != expected.Name: 42 return errorf(actual.Position, 43 "expected field %d to be %s, got %s", 44 i, expected.Name, actual.Name) 45 case actual.Alias != expected.Alias: 46 return errorf(actual.Position, 47 "expected field %d's alias to be %s, got %s", 48 i, expected.Alias, actual.Alias) 49 } 50 err := selectionsMatch(actual.Position, expected.SelectionSet, actual.SelectionSet) 51 if err != nil { 52 return fmt.Errorf("in %s sub-selection: %w", actual.Alias, err) 53 } 54 case *ast.InlineFragment: 55 actual, ok := actualSelectionSet[i].(*ast.InlineFragment) 56 switch { 57 case !ok: 58 return errorf(actual.Position, 59 "expected selection %d to be inline fragment, got %T", 60 i, actualSelectionSet[i]) 61 case actual.TypeCondition != expected.TypeCondition: 62 return errorf(actual.Position, 63 "expected fragment %d to be on type %s, got %s", 64 i, expected.TypeCondition, actual.TypeCondition) 65 } 66 err := selectionsMatch(actual.Position, expected.SelectionSet, actual.SelectionSet) 67 if err != nil { 68 return fmt.Errorf("in inline fragment on %s: %w", actual.TypeCondition, err) 69 } 70 case *ast.FragmentSpread: 71 actual, ok := actualSelectionSet[i].(*ast.FragmentSpread) 72 switch { 73 case !ok: 74 return errorf(actual.Position, 75 "expected selection %d to be fragment spread, got %T", 76 i, actualSelectionSet[i]) 77 case actual.Name != expected.Name: 78 return errorf(actual.Position, 79 "expected fragment %d to be ...%s, got ...%s", 80 i, expected.Name, actual.Name) 81 } 82 } 83 } 84 return nil 85 } 86 87 // validateBindingSelection checks that if you requested in your type-binding 88 // that this type must always request certain fields, then in fact it does. 89 func (g *generator) validateBindingSelection( 90 typeName string, 91 binding *TypeBinding, 92 pos *ast.Position, 93 selectionSet ast.SelectionSet, 94 ) error { 95 if binding.ExpectExactFields == "" { 96 return nil // no validation requested 97 } 98 99 // HACK: we parse the selection as if it were a query, which is basically 100 // the same (for syntax purposes; it of course wouldn't validate) 101 doc, gqlErr := parser.ParseQuery(&ast.Source{Input: binding.ExpectExactFields}) 102 if gqlErr != nil { 103 return errorf( 104 nil, "invalid type-binding %s.expect_exact_fields: %w", typeName, gqlErr) 105 } 106 107 err := selectionsMatch(pos, doc.Operations[0].SelectionSet, selectionSet) 108 if err != nil { 109 return fmt.Errorf("invalid selection for type-binding %s: %w", typeName, err) 110 } 111 return nil 112 }