generates something mostly plausiblegit add .
This commit is contained in:
@@ -13,3 +13,4 @@ Config options:
|
|||||||
|
|
||||||
Misc:
|
Misc:
|
||||||
- replace __ with some unicode garbage
|
- replace __ with some unicode garbage
|
||||||
|
- handle graphql errors in a reasonable way
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Generate the schemas by getting a token from [GitHub](https://github.com/settings/tokens/new) (no scopes needed), then:
|
||||||
|
```
|
||||||
|
npm install -g graphql-introspection-json-to-sdl
|
||||||
|
curl -H "Authorization: bearer <your token>" https://api.github.com/graphql >example/schema.json
|
||||||
|
graphql-introspection-json-to-sdl example/schema.json >example/schema.graphql
|
||||||
|
```
|
||||||
|
TODO: something better
|
||||||
+13
-4
@@ -6,12 +6,14 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/vektah/gqlparser/gqlerror"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GetViewerResponse = struct {
|
type GetViewerResponse = struct {
|
||||||
Viewer struct {
|
Viewer struct {
|
||||||
Name string `json:"name"`
|
Name *string
|
||||||
} `json:"viewer"`
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetViewer gets the current user's name.
|
// GetViewer gets the current user's name.
|
||||||
@@ -42,11 +44,18 @@ query GetViewer {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
retval := GetViewerResponse{}
|
var retval struct {
|
||||||
|
Data GetViewerResponse `json:"data"`
|
||||||
|
Errors gqlerror.List `json:"errors"`
|
||||||
|
}
|
||||||
err = json.Unmarshal(body, &retval)
|
err = json.Unmarshal(body, &retval)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &retval, nil
|
if len(retval.Errors) > 0 {
|
||||||
|
return nil, retval.Errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return &retval.Data, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+23338
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+50
-11
@@ -1,16 +1,20 @@
|
|||||||
package generate
|
package generate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"go/format"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/vektah/gqlparser"
|
||||||
"github.com/vektah/gqlparser/ast"
|
"github.com/vektah/gqlparser/ast"
|
||||||
"github.com/vektah/gqlparser/formatter"
|
"github.com/vektah/gqlparser/formatter"
|
||||||
"github.com/vektah/gqlparser/parser"
|
"github.com/vektah/gqlparser/parser"
|
||||||
|
"github.com/vektah/gqlparser/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: package template into the binary using one of those asset thingies
|
// TODO: package template into the binary using one of those asset thingies
|
||||||
@@ -42,20 +46,43 @@ type OperationParams struct {
|
|||||||
Operation string
|
Operation string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Generate(specFilename, generatedFilename string) error {
|
func Generate(specFilename, schemaFilename, generatedFilename string) error {
|
||||||
text, err := ioutil.ReadFile(specFilename)
|
// 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 {
|
if err != nil {
|
||||||
return fmt.Errorf("could not open query-spec file %v: %v",
|
return fmt.Errorf("unreadable schema file %v: %v",
|
||||||
|
schemaFilename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
schema, graphqlError := gqlparser.LoadSchema(
|
||||||
|
&ast.Source{Name: schemaFilename, Input: string(text)})
|
||||||
|
if graphqlError != nil {
|
||||||
|
return fmt.Errorf("invalid schema file %v: %v",
|
||||||
|
schemaFilename, graphqlError)
|
||||||
|
}
|
||||||
|
|
||||||
|
text, err = ioutil.ReadFile(specFilename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unreadable query-spec file %v: %v",
|
||||||
specFilename, err)
|
specFilename, 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(
|
document, graphqlError := parser.ParseQuery(
|
||||||
&ast.Source{Name: specFilename, Input: string(text)})
|
&ast.Source{Name: specFilename, Input: string(text)})
|
||||||
if graphqlError != nil { // ParseQuery returns type *graphql.Error, yuck
|
if graphqlError != nil { // ParseQuery returns type *graphql.Error, yuck
|
||||||
return fmt.Errorf("could not parse query-spec file %v: %v",
|
return fmt.Errorf("invalid query-spec file %v: %v",
|
||||||
specFilename, graphqlError)
|
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
|
var out io.Writer
|
||||||
if generatedFilename == "-" {
|
if generatedFilename == "-" {
|
||||||
out = os.Stdout
|
out = os.Stdout
|
||||||
@@ -82,11 +109,14 @@ func Generate(specFilename, generatedFilename string) error {
|
|||||||
operations[i] = OperationParams{
|
operations[i] = OperationParams{
|
||||||
OperationType: operation.Operation,
|
OperationType: operation.Operation,
|
||||||
OperationName: operation.Name,
|
OperationName: operation.Name,
|
||||||
OperationDoc: "TODO",
|
// 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.
|
||||||
|
OperationDoc: "TODO",
|
||||||
|
|
||||||
// TODO: configure this
|
// TODO: configure this
|
||||||
ResponseName: operation.Name + "Response",
|
ResponseName: operation.Name + "Response",
|
||||||
ResponseType: "struct{} // TODO",
|
ResponseType: typeFor(operation, schema),
|
||||||
|
|
||||||
Endpoint: endpoint,
|
Endpoint: endpoint,
|
||||||
// The newline just makes it format a little nicer
|
// The newline just makes it format a little nicer
|
||||||
@@ -99,11 +129,19 @@ func Generate(specFilename, generatedFilename string) error {
|
|||||||
Operations: operations,
|
Operations: operations,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tmpl.Execute(out, data)
|
var buf bytes.Buffer
|
||||||
|
err = tmpl.Execute(&buf, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not render template: %v", err)
|
return fmt.Errorf("could not render template: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
formatted, err := format.Source(buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not gofmt template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = out.Write(formatted)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func Main() {
|
func Main() {
|
||||||
@@ -115,9 +153,10 @@ func Main() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if len(os.Args) != 3 {
|
if len(os.Args) != 4 {
|
||||||
err = fmt.Errorf("usage: %s queries.graphql generated.go", os.Args[0])
|
err = fmt.Errorf("usage: %s queries.graphql schema.graphqll generated.go",
|
||||||
|
os.Args[0])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = Generate(os.Args[1], os.Args[2])
|
err = Generate(os.Args[1], os.Args[2], os.Args[3])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/vektah/gqlparser/gqlerror"
|
||||||
)
|
)
|
||||||
|
|
||||||
{{range .Operations}}
|
{{range .Operations}}
|
||||||
@@ -33,12 +35,19 @@ func {{.OperationName}}(ctx context.Context) (*{{.ResponseName}}, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
retval := {{.ResponseName}}{}
|
var retval struct {
|
||||||
|
Data {{.ResponseName}} `json:"data"`
|
||||||
|
Errors gqlerror.List `json:"errors"`
|
||||||
|
}
|
||||||
err = json.Unmarshal(body, &retval)
|
err = json.Unmarshal(body, &retval)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &retval, nil
|
if len(retval.Errors) > 0 {
|
||||||
|
return nil, retval.Errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return &retval.Data, nil
|
||||||
}
|
}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package generate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/vektah/gqlparser/ast"
|
||||||
|
)
|
||||||
|
|
||||||
|
func typeFor(operation *ast.OperationDefinition, schema *ast.Schema) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
writeSelectionSetStruct(&builder, operation.SelectionSet, schema)
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeSelectionSetStruct(builder *strings.Builder, selectionSet ast.SelectionSet, schema *ast.Schema) {
|
||||||
|
builder.WriteString("struct {\n")
|
||||||
|
for _, selection := range selectionSet {
|
||||||
|
switch selection := selection.(type) {
|
||||||
|
case *ast.Field:
|
||||||
|
// TODO: assert it starts with uppercase (or do automatically if
|
||||||
|
// alias is not provided; in this case we may need json tags to
|
||||||
|
// avoid munging the query)
|
||||||
|
if selection.Alias != "" {
|
||||||
|
builder.WriteString(selection.Alias)
|
||||||
|
} else {
|
||||||
|
builder.WriteString(selection.Name)
|
||||||
|
}
|
||||||
|
builder.WriteRune(' ')
|
||||||
|
|
||||||
|
writeType(builder, selection.Definition.Type, selection.SelectionSet, schema)
|
||||||
|
|
||||||
|
// We don't need a json tag -- we just have GraphQL do the
|
||||||
|
// aliasing.
|
||||||
|
builder.WriteRune('\n')
|
||||||
|
|
||||||
|
case *ast.FragmentSpread, *ast.InlineFragment:
|
||||||
|
panic("TODO")
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("invalid selection type: %v", selection))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.WriteString("}")
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeType(builder *strings.Builder, typ *ast.Type, selectionSet ast.SelectionSet, schema *ast.Schema) {
|
||||||
|
if typ.Elem != nil {
|
||||||
|
// Type is a list.
|
||||||
|
builder.WriteString("[]")
|
||||||
|
typ = typ.Elem
|
||||||
|
} else if !typ.NonNull { // no need for pointer if we have a list
|
||||||
|
builder.WriteString("*")
|
||||||
|
}
|
||||||
|
|
||||||
|
if selectionSet != nil {
|
||||||
|
writeSelectionSetStruct(builder, selectionSet, schema)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: actually handle scalars. or can we instead use gqlgen's
|
||||||
|
// converter? they're doing mostly the same thing. if not, crib from it:
|
||||||
|
// https://github.com/99designs/gqlgen/blob/master/plugin/modelgen/models.go#L113
|
||||||
|
builder.WriteString(strings.ToLower(typ.Name()))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user