diff --git a/cmd/genql/main.go b/cmd/genql/main.go index 062335f..0620d99 100644 --- a/cmd/genql/main.go +++ b/cmd/genql/main.go @@ -1,16 +1,9 @@ package main import ( - "fmt" - "os" - "github.com/Khan/genql/generate" ) func main() { - err := generate.Generate() - if err != nil { - fmt.Println(fmt.Errorf("genql failed: %v", err)) - os.Exit(1) - } + generate.Main() } diff --git a/example/generate_me.go b/example/generate_me.go index 1c40cf1..eb2d9c1 100644 --- a/example/generate_me.go +++ b/example/generate_me.go @@ -20,11 +20,10 @@ func GetViewer(ctx context.Context) (*GetViewerResponse, error) { http.MethodPost, `https://api.github.com/graphql`, strings.NewReader(` -"GetViewer gets the current user's name." query GetViewer { - Viewer: viewer { - Name: name - } + Viewer: viewer { + Name: name + } } `)) if err != nil { diff --git a/example/queries.graphql b/example/queries.graphql index 42490bc..10f97d7 100644 --- a/example/queries.graphql +++ b/example/queries.graphql @@ -1,4 +1,4 @@ -"GetViewer gets the current user's name." +# GetViewer gets the current user's name. query GetViewer { Viewer: viewer { Name: name diff --git a/generate/generate.go b/generate/generate.go index 34cc875..726766f 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -2,41 +2,122 @@ package generate import ( "fmt" + "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" ) -var _ = gqlparser.LoadSchema - // TODO: package template into the binary using one of those asset thingies const tmplFilename = "generate/operation.go.tmpl" +var tmpl = template.Must(template.ParseFiles(tmplFilename)) + 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 []OperationParams +} + +type OperationParams struct { // The type-name for the operation's response type. ResponseName string // The body of the operation's response type (e.g. struct { ... }). ResponseType string - // The documentation for the operation, from GraphQL. - OperationDoc string + // The type of the operation (query, mutation, or subscription). + OperationType ast.Operation // The name of the operation, from GraphQL. OperationName string + // The documentation for the operation, from GraphQL. + OperationDoc string // The endpoint to which to send queries. Endpoint string // The body of the operation to send. Operation string } -var tmpl = template.Must(template.ParseFiles(tmplFilename)) - -func Generate() error { - var data TemplateParams - err := tmpl.Execute(os.Stdout, data) +func Generate(specFilename, generatedFilename string) error { + text, err := ioutil.ReadFile(specFilename) if err != nil { - return fmt.Errorf("template did not render: %v", err) + return fmt.Errorf("could not open query-spec file %v: %v", + specFilename, err) + } + + document, graphqlError := parser.ParseQuery( + &ast.Source{Name: specFilename, Input: string(text)}) + if graphqlError != nil { // ParseQuery returns type *graphql.Error, yuck + return fmt.Errorf("could not parse query-spec file %v: %v", + specFilename, graphqlError) + } + + var out io.Writer + if generatedFilename == "-" { + out = os.Stdout + } else { + out, err = os.OpenFile(generatedFilename, os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + return fmt.Errorf("could not open generated file %v: %v", + generatedFilename, err) + } + } + + // TODO: configure these + packageName := "example" + endpoint := "https://api.github.com/graphql" + + operations := make([]OperationParams, len(document.Operations)) + for i, operation := range document.Operations { + var builder strings.Builder + f := formatter.NewFormatter(&builder) + f.FormatQueryDocument(&ast.QueryDocument{ + Operations: ast.OperationList{operation}, + // TODO: handle fragments + }) + operations[i] = OperationParams{ + OperationType: operation.Operation, + OperationName: operation.Name, + OperationDoc: "TODO", + + // TODO: configure this + ResponseName: operation.Name + "Response", + ResponseType: "struct{} // TODO", + + Endpoint: endpoint, + // The newline just makes it format a little nicer + Operation: "\n" + builder.String(), + } + } + + data := TemplateParams{ + PackageName: packageName, + Operations: operations, + } + + err = tmpl.Execute(out, data) + if err != nil { + return fmt.Errorf("could not render template: %v", err) } return nil } + +func Main() { + var err error + defer func() { + if err != nil { + fmt.Println(err) + os.Exit(1) + } + }() + + if len(os.Args) != 3 { + err = fmt.Errorf("usage: %s queries.graphql generated.go", os.Args[0]) + return + } + err = Generate(os.Args[1], os.Args[2]) +} diff --git a/generate/operation.go.tmpl b/generate/operation.go.tmpl index 881ddd5..3f473ee 100644 --- a/generate/operation.go.tmpl +++ b/generate/operation.go.tmpl @@ -8,6 +8,7 @@ import ( "strings" ) +{{range .Operations}} type {{.ResponseName}} = {{.ResponseType}} // {{.OperationDoc}} @@ -20,7 +21,7 @@ func {{.OperationName}}(ctx context.Context) (*{{.ResponseName}}, error) { return nil, err } - req = req.WithContext(ctx) + req = req.WithContext(ctx) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err @@ -40,3 +41,4 @@ func {{.OperationName}}(ctx context.Context) (*{{.ResponseName}}, error) { return &retval, nil } +{{end}}