diff --git a/README.md b/README.md index 11e3c08..5db59d9 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ TODO(benkraft): Figure out how to get GitHub Actions to run the example -- it ne ## Major TODOs Query structures to support: +- interfaces +- unions - fragments Config options: diff --git a/generate/generate.go b/generate/generate.go index 5b4b4a3..1784779 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -29,9 +29,8 @@ type generator struct { // The list of operations for which to generate code. Operations []operation // The types needed for these operations. - typeMap map[string]string - schema *ast.Schema - fragments []*ast.FragmentDefinition + typeMap map[string]string + schema *ast.Schema } type operation struct { @@ -112,15 +111,12 @@ func (g *generator) addOperation(op *ast.OperationDefinition) error { // TODO: we may have to actually get the precise query text, in case we // want to be hashing it or something like that. This is a bit tricky // because gqlparser's ast doesn't provide node end-position (only - // token end-position). (And with fragments it's not even clear what would - // be right.) Maybe add as a config option, and allow only if the document - // has exactly one query? + // token end-position). var builder strings.Builder f := formatter.NewFormatter(&builder) f.FormatQueryDocument(&ast.QueryDocument{ Operations: ast.OperationList{op}, - // TODO(benkraft): Only include relevant fragments. - Fragments: g.fragments, + // TODO: handle fragments }) args := make([]argument, len(op.VariableDefinitions)) @@ -150,7 +146,7 @@ func (g *generator) addOperation(op *ast.OperationDefinition) error { return nil } -func generate(config *Config) (*generator, error) { +func Generate(config *Config) ([]byte, error) { schema, err := getSchema(config.Schema) if err != nil { return nil, err @@ -162,26 +158,12 @@ func generate(config *Config) (*generator, error) { } g := newGenerator(config, schema) - for _, frag := range document.Fragments { - if err = g.addFragment(frag); err != nil { - return nil, err - } - } for _, op := range document.Operations { if err = g.addOperation(op); err != nil { return nil, err } } - return g, nil -} - -func Generate(config *Config) ([]byte, error) { - g, err := generate(config) - if err != nil { - return nil, err - } - var buf bytes.Buffer err = tmpl.Execute(&buf, g) if err != nil { diff --git a/generate/testdata/InterfaceNoFragments.graphql b/generate/testdata/InterfaceNoFragments.graphql deleted file mode 100644 index b46a8d8..0000000 --- a/generate/testdata/InterfaceNoFragments.graphql +++ /dev/null @@ -1,10 +0,0 @@ -{ - root { - id - name - children { - id - name - } - } -} diff --git a/generate/testdata/InterfaceNoFragments.graphql.go b/generate/testdata/InterfaceNoFragments.graphql.go deleted file mode 100644 index cf9dc73..0000000 --- a/generate/testdata/InterfaceNoFragments.graphql.go +++ /dev/null @@ -1,10 +0,0 @@ -type Response struct { - Root struct { - Id string `json:"id"` - Name string `json:"name"` - Children []struct { - Id string `json:"id"` - Name string `json:"name"` - } `json:"children"` - } `json:"root"` -} \ No newline at end of file diff --git a/generate/testdata/SimpleFragment.graphql b/generate/testdata/SimpleFragment.graphql deleted file mode 100644 index 95609d6..0000000 --- a/generate/testdata/SimpleFragment.graphql +++ /dev/null @@ -1,11 +0,0 @@ -fragment profileData on User { - name - emails -} - -query { - user { - id - ...profileData - } -} diff --git a/generate/testdata/SimpleFragment.graphql.go b/generate/testdata/SimpleFragment.graphql.go deleted file mode 100644 index 967f161..0000000 --- a/generate/testdata/SimpleFragment.graphql.go +++ /dev/null @@ -1,11 +0,0 @@ -type Response struct { - User *struct { - Id string `json:"id"` - profileData - } `json:"user"` -} - -type profileData struct { - Name *string `json:"name"` - Emails []string `json:"emails"` -} \ No newline at end of file diff --git a/generate/testdata/UnionNoFragments.graphql b/generate/testdata/UnionNoFragments.graphql deleted file mode 100644 index 0f4336b..0000000 --- a/generate/testdata/UnionNoFragments.graphql +++ /dev/null @@ -1,5 +0,0 @@ -{ - randomLeaf { - __typename - } -} diff --git a/generate/testdata/UnionNoFragments.graphql.go b/generate/testdata/UnionNoFragments.graphql.go deleted file mode 100644 index d737f74..0000000 --- a/generate/testdata/UnionNoFragments.graphql.go +++ /dev/null @@ -1,5 +0,0 @@ -type Response struct { - RandomLeaf struct { - Typename *string `json:"__typename"` - } `json:"randomLeaf"` -} \ No newline at end of file diff --git a/generate/types.go b/generate/types.go index ab62bda..486f95c 100644 --- a/generate/types.go +++ b/generate/types.go @@ -44,18 +44,6 @@ func (g *generator) getTypeForOperation(operation *ast.OperationDefinition) (nam name, g.baseTypeForOperation(operation.Operation), selectionSet) } -func (g *generator) addFragment(frag *ast.FragmentDefinition) error { - g.fragments = append(g.fragments, frag) - - selectionSet, err := selections(frag.SelectionSet) - if err != nil { - return err - } - - _, err = g.addTypeForDefinition(lowerFirst(frag.Name), frag.Definition, selectionSet) - return err -} - func (g *generator) addTypeForDefinition(nameOverride string, typ *ast.Definition, selectionSet []selection) (name string, err error) { goName, ok := builtinTypes[typ.Name] if ok { @@ -86,12 +74,31 @@ func (g *generator) addTypeForDefinition(nameOverride string, typ *ast.Definitio func (g *generator) getTypeForInputType(typ *ast.Type) (string, error) { builder := &typeBuilder{typeName: lowerFirst(typ.Name()), generator: g} - err := builder.writeType(typ, g.selectionsForInputType(typ), false) + err := builder.writeType(typ, selectionsForType(g, typ), false) return builder.String(), err } type selection interface { - Write(builder *typeBuilder) error + Alias() string + Name() string + Type() *ast.Type + SelectionSet() ([]selection, error) +} + +type field struct{ field *ast.Field } + +func (s field) Alias() string { return s.field.Alias } +func (s field) Name() string { return s.field.Name } + +func (s field) Type() *ast.Type { + if s.field.Definition == nil { + return nil + } + return s.field.Definition.Type +} + +func (s field) SelectionSet() ([]selection, error) { + return selections(s.field.SelectionSet) } func selections(selectionSet ast.SelectionSet) ([]selection, error) { @@ -100,9 +107,7 @@ func selections(selectionSet ast.SelectionSet) ([]selection, error) { switch selection := selection.(type) { case *ast.Field: retval[i] = field{selection} - case *ast.FragmentSpread: - retval[i] = fragmentSpread{selection} - case *ast.InlineFragment: + case *ast.FragmentSpread, *ast.InlineFragment: return nil, fmt.Errorf("not implemented: %T", selection) default: return nil, fmt.Errorf("invalid selection type: %v", selection) @@ -111,47 +116,20 @@ func selections(selectionSet ast.SelectionSet) ([]selection, error) { return retval, nil } -type field struct{ field *ast.Field } - -func (s field) Write(builder *typeBuilder) error { - if s.field.Definition == nil || s.field.Definition.Type == nil { - // Unclear why gqlparser hasn't already rejected this, - // but empirically it might not. - return fmt.Errorf("undefined field %v", s.field.Name) - } - - jsonName := s.field.Alias - if jsonName == "" { - // TODO(benkraft): Does this actually happen? Tests suggest not. - jsonName = s.field.Name - } - - selectionSet, err := selections(s.field.SelectionSet) - if err != nil { - return err - } - - return builder.writeField(jsonName, s.field.Definition.Type, selectionSet) -} - -type fragmentSpread struct{ frag *ast.FragmentSpread } - -func (s fragmentSpread) Write(builder *typeBuilder) error { - builder.WriteString(lowerFirst(s.frag.Name)) - return nil -} - type inputField struct { - g *generator + *generator field *ast.FieldDefinition } -func (s inputField) Write(builder *typeBuilder) error { - selectionSet := s.g.selectionsForInputType(s.field.Type) - return builder.writeField(s.field.Name, s.field.Type, selectionSet) +func (s inputField) Alias() string { return s.field.Name } +func (s inputField) Name() string { return s.field.Name } +func (s inputField) Type() *ast.Type { return s.field.Type } + +func (s inputField) SelectionSet() ([]selection, error) { + return selectionsForType(s.generator, s.field.Type), nil } -func (g *generator) selectionsForInputType(typ *ast.Type) []selection { +func selectionsForType(g *generator, typ *ast.Type) []selection { def := g.schema.Types[typ.Name()] selectionSet := make([]selection, len(def.Fields)) for i, field := range def.Fields { @@ -160,14 +138,33 @@ func (g *generator) selectionsForInputType(typ *ast.Type) []selection { return selectionSet } -func (builder *typeBuilder) writeField(jsonName string, typ *ast.Type, selectionSet []selection) error { +func (builder *typeBuilder) writeField(selection selection) error { + var jsonName string + if selection.Alias() != "" { + jsonName = selection.Alias() + } else { + // TODO: is this case needed? tests don't seem to get here. + jsonName = selection.Name() + } // We need an exportable name for JSON-marshaling. goName := upperFirst(jsonName) builder.WriteString(goName) builder.WriteRune(' ') - err := builder.writeType(typ, selectionSet, true) + typ := selection.Type() + if typ == nil { + // Unclear why gqlparser hasn't already rejected this, + // but empirically it might not. + return fmt.Errorf("undefined field %v", selection.Name()) + } + + selectionSet, err := selection.SelectionSet() + if err != nil { + return err + } + + err = builder.writeType(typ, selectionSet, true) if err != nil { return err } @@ -175,6 +172,7 @@ func (builder *typeBuilder) writeField(jsonName string, typ *ast.Type, selection if jsonName != goName { fmt.Fprintf(builder, " `json:\"%s\"`", jsonName) } + builder.WriteRune('\n') return nil } @@ -202,8 +200,9 @@ func (builder *typeBuilder) writeType(typ *ast.Type, selectionSet []selection, i } def := builder.schema.Types[typ.Name()] + // TODO: set inline = false for nested types switch def.Kind { - case ast.Scalar, ast.Enum: + case ast.Scalar, ast.Enum, ast.Union, ast.Interface: inline = false } @@ -223,11 +222,10 @@ func (builder *typeBuilder) writeType(typ *ast.Type, selectionSet []selection, i func (builder *typeBuilder) writeTypedef(typedef *ast.Definition, selectionSet []selection) error { switch typedef.Kind { - case ast.Object, ast.InputObject, ast.Interface, ast.Union: + case ast.Object, ast.InputObject: builder.WriteString("struct {\n") for _, field := range selectionSet { - err := field.Write(builder) - builder.WriteRune('\n') + err := builder.writeField(field) if err != nil { return err } @@ -246,7 +244,7 @@ func (builder *typeBuilder) writeTypedef(typedef *ast.Definition, selectionSet [ } builder.WriteString(")\n") return nil - case ast.Scalar: + case ast.Scalar, ast.Union, ast.Interface: // TODO(benkraft): Handle custom scalars, unions, and interfaces. return fmt.Errorf("not implemented: %v", typedef.Kind) default: diff --git a/generate/types_test.go b/generate/types_test.go index 29dc51f..bcff5d9 100644 --- a/generate/types_test.go +++ b/generate/types_test.go @@ -55,6 +55,8 @@ func TestTypeForOperation(t *testing.T) { t.Fatal(err) } + schemaText := readFile(t, "schema.graphql", false) + for _, file := range files { graphqlFilename := file.Name() if graphqlFilename == "schema.graphql" || !strings.HasSuffix(graphqlFilename, ".graphql") { @@ -68,10 +70,27 @@ func TestTypeForOperation(t *testing.T) { t.Fatal(err) } - g, err := generate(&Config{ - Schema: filepath.Join("testdata", "schema.graphql"), - Queries: filepath.Join("testdata", graphqlFilename), - }) + schema, graphqlError := gqlparser.LoadSchema( + &ast.Source{Name: "test schema", Input: schemaText}) + if graphqlError != nil { + t.Fatal(graphqlError) + } + + queryDoc, graphqlListError := gqlparser.LoadQuery( + schema, readFile(t, graphqlFilename, false)) + if graphqlListError != nil { + t.Fatal(graphqlListError) + } + + if len(queryDoc.Operations) != 1 { + t.Fatalf("got %v operations, want 1", len(queryDoc.Operations)) + } + + g := newGenerator(&Config{Package: "test_package"}, schema) + err = g.addOperation(queryDoc.Operations[0]) + if err != nil { + t.Error(err) + } // gofmt before comparing. goType, err := gofmt(g.Types())