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  }