diff --git a/generate/generate.go b/generate/generate.go index 1784779..5b4b4a3 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -29,8 +29,9 @@ 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 + typeMap map[string]string + schema *ast.Schema + fragments []*ast.FragmentDefinition } type operation struct { @@ -111,12 +112,15 @@ 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). + // 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? var builder strings.Builder f := formatter.NewFormatter(&builder) f.FormatQueryDocument(&ast.QueryDocument{ Operations: ast.OperationList{op}, - // TODO: handle fragments + // TODO(benkraft): Only include relevant fragments. + Fragments: g.fragments, }) args := make([]argument, len(op.VariableDefinitions)) @@ -146,7 +150,7 @@ func (g *generator) addOperation(op *ast.OperationDefinition) error { return nil } -func Generate(config *Config) ([]byte, error) { +func generate(config *Config) (*generator, error) { schema, err := getSchema(config.Schema) if err != nil { return nil, err @@ -158,12 +162,26 @@ func Generate(config *Config) ([]byte, 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/SimpleFragment.graphql b/generate/testdata/SimpleFragment.graphql new file mode 100644 index 0000000..95609d6 --- /dev/null +++ b/generate/testdata/SimpleFragment.graphql @@ -0,0 +1,11 @@ +fragment profileData on User { + name + emails +} + +query { + user { + id + ...profileData + } +} diff --git a/generate/testdata/SimpleFragment.graphql.go b/generate/testdata/SimpleFragment.graphql.go new file mode 100644 index 0000000..f2e8306 --- /dev/null +++ b/generate/testdata/SimpleFragment.graphql.go @@ -0,0 +1,11 @@ +type ProfileData struct { + Name *string `json:"name"` + Emails []string `json:"emails"` +} + +type Response struct { + User *struct { + Id string `json:"id"` + ProfileData + } `json:"user"` +} \ No newline at end of file diff --git a/generate/types.go b/generate/types.go index 3923892..4d92f70 100644 --- a/generate/types.go +++ b/generate/types.go @@ -44,6 +44,18 @@ 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(upperFirst(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 { @@ -101,13 +113,25 @@ func (s field) SelectionSet() ([]selection, error) { return selections(s.field.SelectionSet) } +type fragmentSpread struct{ frag *ast.FragmentSpread } + +func (s fragmentSpread) Name() string { return upperFirst(s.frag.Name) } + +// TODO(benkraft): These methods aren't actually called; refactor so that they +// aren't needed +func (s fragmentSpread) Alias() string { panic("TODO") } +func (s fragmentSpread) Type() *ast.Type { panic("TODO") } +func (s fragmentSpread) SelectionSet() ([]selection, error) { panic("TODO") } + func selections(selectionSet ast.SelectionSet) ([]selection, error) { retval := make([]selection, len(selectionSet)) for i, selection := range selectionSet { switch selection := selection.(type) { case *ast.Field: retval[i] = field{selection} - case *ast.FragmentSpread, *ast.InlineFragment: + case *ast.FragmentSpread: + retval[i] = fragmentSpread{selection} + case *ast.InlineFragment: return nil, fmt.Errorf("not implemented: %T", selection) default: return nil, fmt.Errorf("invalid selection type: %v", selection) @@ -139,6 +163,15 @@ func selectionsForType(g *generator, typ *ast.Type) []selection { } func (builder *typeBuilder) writeField(selection selection) error { + // Fragments have no GraphQL type to write; and the Go type is already + // defined, so we just handle that specially. + // TODO(benkraft): This is a terrible hack; refactor so it's not necessary. + if frag, ok := selection.(fragmentSpread); ok { + builder.WriteString(frag.Name()) + builder.WriteRune('\n') + return nil + } + var jsonName string if selection.Alias() != "" { jsonName = selection.Alias() diff --git a/generate/types_test.go b/generate/types_test.go index bcff5d9..29dc51f 100644 --- a/generate/types_test.go +++ b/generate/types_test.go @@ -55,8 +55,6 @@ 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") { @@ -70,27 +68,10 @@ func TestTypeForOperation(t *testing.T) { t.Fatal(err) } - 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) - } + g, err := generate(&Config{ + Schema: filepath.Join("testdata", "schema.graphql"), + Queries: filepath.Join("testdata", graphqlFilename), + }) // gofmt before comparing. goType, err := gofmt(g.Types())