From 0bce8967222ea14ae2e2f698139e04193fc6e7f0 Mon Sep 17 00:00:00 2001 From: Ben Kraft Date: Thu, 2 Jan 2020 18:22:19 -0800 Subject: [PATCH] break up Generate --- generate/generate.go | 171 +++++++++++++------------------------------ generate/main.go | 63 ++++++++++++++++ generate/parse.go | 52 +++++++++++++ go.sum | 6 ++ 4 files changed, 173 insertions(+), 119 deletions(-) create mode 100644 generate/main.go create mode 100644 generate/parse.go diff --git a/generate/generate.go b/generate/generate.go index e6d02c4..9595589 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -4,17 +4,11 @@ import ( "bytes" "fmt" "go/format" - "io" - "io/ioutil" - "os" "strings" "text/template" - "github.com/vektah/gqlparser" "github.com/vektah/gqlparser/ast" "github.com/vektah/gqlparser/formatter" - "github.com/vektah/gqlparser/parser" - "github.com/vektah/gqlparser/validator" ) // TODO: package template into the binary using one of those asset thingies @@ -22,14 +16,14 @@ const tmplFilename = "generate/operation.go.tmpl" var tmpl = template.Must(template.ParseFiles(tmplFilename)) -type TemplateParams struct { +type templateParams struct { // The name of the package into which to generate the operation-helpers. PackageName string // The list of operations for which to generate code. - Operations []Operation + Operations []operation } -type Operation struct { +type operation struct { // The type of the operation (query, mutation, or subscription). Type ast.Operation // The name of the operation, from GraphQL. @@ -39,7 +33,7 @@ type Operation struct { // The body of the operation to send. Body string // The arguments to the operation. - Args []Argument + Args []argument // The type-name for the operation's response type. ResponseName string @@ -47,138 +41,77 @@ type Operation struct { ResponseType string } -type Argument struct { +type argument struct { GoName string GoType string GraphQLName string } -func Generate(specFilename, schemaFilename, generatedFilename string) error { - // TODO: all the read-parse-and-validate stuff can probably get factored - // out a bit - // TODO: IRL we have to get the schema from GraphQL (maybe we can generate - // that once we can bootstrap) where it comes as JSON, not SDL, so we have - // to convert (or add gqlparser support to convert) - text, err := ioutil.ReadFile(schemaFilename) - if err != nil { - return fmt.Errorf("unreadable schema file %v: %v", - schemaFilename, err) +func fromASTArg(arg *ast.VariableDefinition, schema *ast.Schema) argument { + return argument{ + GraphQLName: arg.Variable, + GoName: arg.Variable, // TODO: normalize this to go-style + GoType: typeForInputType(arg.Type, schema), + // TODO: figure out what to do about defaults } +} - schema, graphqlError := gqlparser.LoadSchema( - &ast.Source{Name: schemaFilename, Input: string(text)}) - if graphqlError != nil { - return fmt.Errorf("invalid schema file %v: %v", - schemaFilename, graphqlError) +func fromASTOperation(op *ast.OperationDefinition, schema *ast.Schema) operation { + // TODO: we may have to actually get the precise query text, in case we + // want to be hashing it or something like that. Although maybe + // there's no reasonable way to do that with several queries in one + // file. + var builder strings.Builder + f := formatter.NewFormatter(&builder) + f.FormatQueryDocument(&ast.QueryDocument{ + Operations: ast.OperationList{op}, + // TODO: handle fragments + }) + + args := make([]argument, len(op.VariableDefinitions)) + for i, arg := range op.VariableDefinitions { + args[i] = fromASTArg(arg, schema) } + return operation{ + Type: op.Operation, + Name: op.Name, + // TODO: this is actually awkward, because GraphQL doesn't allow + // for docstrings on queries (only schemas). So we have to extract + // the comment, or omit doc-comments for now. + Doc: "TODO", + // The newline just makes it format a little nicer + Body: "\n" + builder.String(), + Args: args, - text, err = ioutil.ReadFile(specFilename) - if err != nil { - return fmt.Errorf("unreadable query-spec file %v: %v", - specFilename, err) + // TODO: configure ResponseName format + ResponseName: op.Name + "Response", + ResponseType: typeForOperation(op, schema), } +} - // The following is more or less gqlparser.LoadQuery, but we can provide a - // name so we might as well (and we break out the two errors). - document, graphqlError := parser.ParseQuery( - &ast.Source{Name: specFilename, Input: string(text)}) - if graphqlError != nil { // ParseQuery returns type *graphql.Error, yuck - return fmt.Errorf("invalid query-spec file %v: %v", - specFilename, graphqlError) - } - - graphqlErrors := validator.Validate(schema, document) - if graphqlErrors != nil { - return fmt.Errorf("query-spec does not match schema: %v", graphqlErrors) - } - - var out io.Writer - if generatedFilename == "-" { - out = os.Stdout - } else { - out, err = os.OpenFile(generatedFilename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { - return fmt.Errorf("could not open generated file %v: %v", - generatedFilename, err) - } - } - - // TODO: configure this - packageName := "example" - +func Generate(schema *ast.Schema, document *ast.QueryDocument) ([]byte, error) { // TODO: this should probably get factored out - operations := make([]Operation, len(document.Operations)) - for i, operation := range document.Operations { - // TODO: we may have to actually get the precise query text, in case we - // want to be hashing it or something like that. Although maybe - // there's no reasonable way to do that with several queries in one - // file. - var builder strings.Builder - f := formatter.NewFormatter(&builder) - f.FormatQueryDocument(&ast.QueryDocument{ - Operations: ast.OperationList{operation}, - // TODO: handle fragments - }) - - args := make([]Argument, len(operation.VariableDefinitions)) - for i, arg := range operation.VariableDefinitions { - args[i] = Argument{ - GraphQLName: arg.Variable, - GoName: arg.Variable, // TODO: normalize this to go-style - GoType: typeForInputType(arg.Type, schema), - // TODO: figure out what to do about defaults - } - } - operations[i] = Operation{ - Type: operation.Operation, - Name: operation.Name, - // TODO: this is actually awkward, because GraphQL doesn't allow - // for docstrings on queries (only schemas). So we have to extract - // the comment, or omit doc-comments for now. - Doc: "TODO", - // The newline just makes it format a little nicer - Body: "\n" + builder.String(), - Args: args, - - // TODO: configure ResponseName format - ResponseName: operation.Name + "Response", - ResponseType: typeForOperation(operation, schema), - } + operations := make([]operation, len(document.Operations)) + for i, op := range document.Operations { + operations[i] = fromASTOperation(op, schema) } - data := TemplateParams{ - PackageName: packageName, + data := templateParams{ + // TODO: configure PackageName + PackageName: "example", Operations: operations, } var buf bytes.Buffer - err = tmpl.Execute(&buf, data) + err := tmpl.Execute(&buf, data) if err != nil { - return fmt.Errorf("could not render template: %v", err) + return nil, fmt.Errorf("could not render template: %v", err) } formatted, err := format.Source(buf.Bytes()) if err != nil { - return fmt.Errorf("could not gofmt template: %v", err) + return nil, fmt.Errorf("could not gofmt template: %v", err) } - _, err = out.Write(formatted) - return err -} - -func Main() { - var err error - defer func() { - if err != nil { - fmt.Println(err) - os.Exit(1) - } - }() - - if len(os.Args) != 4 { - err = fmt.Errorf("usage: %s queries.graphql schema.graphql generated.go", - os.Args[0]) - return - } - err = Generate(os.Args[1], os.Args[2], os.Args[3]) + return formatted, nil } diff --git a/generate/main.go b/generate/main.go new file mode 100644 index 0000000..f69b9c7 --- /dev/null +++ b/generate/main.go @@ -0,0 +1,63 @@ +package generate + +import ( + "fmt" + "io" + "os" +) + +func outputWriter(filename string) (io.Writer, error) { + if filename == "-" { + return os.Stdout, nil + } + + f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return nil, fmt.Errorf("could not open generated file %v: %v", + filename, err) + } + return f, nil +} + +func ParseGenerateAndWrite(specFilename, schemaFilename string, out io.Writer) error { + schema, err := getSchema(schemaFilename) + if err != nil { + return err + } + + document, err := getAndValidateQueries(specFilename, schema) + if err != nil { + return err + } + + code, err := Generate(schema, document) + if err != nil { + return err + } + + _, err = out.Write(code) + return err +} + +func Main() { + var err error + defer func() { + if err != nil { + fmt.Println(err) + os.Exit(1) + } + }() + + if len(os.Args) != 4 { + err = fmt.Errorf("usage: %s queries.graphql schema.graphql generated.go", + os.Args[0]) + return + } + + out, err := outputWriter(os.Args[3]) + if err != nil { + return + } + + err = ParseGenerateAndWrite(os.Args[1], os.Args[2], out) +} diff --git a/generate/parse.go b/generate/parse.go new file mode 100644 index 0000000..51f3e5e --- /dev/null +++ b/generate/parse.go @@ -0,0 +1,52 @@ +package generate + +import ( + "fmt" + "io/ioutil" + + "github.com/vektah/gqlparser" + "github.com/vektah/gqlparser/ast" + "github.com/vektah/gqlparser/parser" + "github.com/vektah/gqlparser/validator" +) + +func getSchema(filename string) (*ast.Schema, error) { + // TODO: IRL we have to get the schema from GraphQL (maybe we can generate + // that once we can bootstrap) where it comes as JSON, not SDL, so we have + // to convert (or add gqlparser support to convert) + text, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("unreadable schema file %v: %v", filename, err) + } + + schema, graphqlError := gqlparser.LoadSchema( + &ast.Source{Name: filename, Input: string(text)}) + if graphqlError != nil { + return nil, fmt.Errorf("invalid schema file %v: %v", + filename, graphqlError) + } + + return schema, nil +} + +func getAndValidateQueries(filename string, schema *ast.Schema) (*ast.QueryDocument, error) { + text, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("unreadable query-spec file %v: %v", filename, err) + } + + // The following is more or less gqlparser.LoadQuery, but we can provide a + // name so we might as well (and we break out the two errors). + document, graphqlError := parser.ParseQuery( + &ast.Source{Name: filename, Input: string(text)}) + if graphqlError != nil { // ParseQuery returns type *graphql.Error, yuck + return nil, fmt.Errorf("invalid query-spec file %v: %v", filename, graphqlError) + } + + graphqlErrors := validator.Validate(schema, document) + if graphqlErrors != nil { + return nil, fmt.Errorf("query-spec does not match schema: %v", graphqlErrors) + } + + return document, nil +} diff --git a/go.sum b/go.sum index 22255cf..818b0ef 100644 --- a/go.sum +++ b/go.sum @@ -2,13 +2,18 @@ github.com/Khan/graphql v0.0.0-20191109005718-3b51154b2bc5 h1:C7qbo6snTa0/lSpVKY github.com/Khan/graphql v0.0.0-20191109005718-3b51154b2bc5/go.mod h1:DLkRTcWV7KR4zw1iTm1lpShI3XP3nFmIMs3fHxYAsF0= github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk= github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/vektah/gqlparser v1.2.0 h1:ntkSCX7F5ZJKl+HIVnmLaO269MruasVpNiMOjX9kgo0= github.com/vektah/gqlparser v1.2.0/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74= @@ -19,4 +24,5 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=