Adnan Maolood: 1 cmd/gqlintrospect: new command 4 files changed, 404 insertions(+), 0 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~emersion/public-inbox/patches/33798/mbox | git am -3Learn more about email & git
--- v3: move types to types.go, support GraphQL June2018 and up, fix input field printing TODO: drop hut/termfmt dependency cmd/gqlintrospect/main.go | 306 +++++++++++++++++++++++++++++++++++++ cmd/gqlintrospect/types.go | 89 +++++++++++ go.mod | 3 + go.sum | 6 + 4 files changed, 404 insertions(+) create mode 100644 cmd/gqlintrospect/main.go create mode 100644 cmd/gqlintrospect/types.go diff --git a/cmd/gqlintrospect/main.go b/cmd/gqlintrospect/main.go new file mode 100644 index 0000000..a043fc0 --- /dev/null +++ b/cmd/gqlintrospect/main.go @@ -0,0 +1,306 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "net/http" + "strings" + "time" + + "git.sr.ht/~emersion/gqlclient" + "git.sr.ht/~emersion/hut/termfmt" +) + +// The query generated by graphiql to determine type information +const query = ` +query IntrospectionQuery { + __schema { + queryType { + name + } + mutationType { + name + } + subscriptionType { + name + } + types { + ...FullType + } + directives { + name + description + locations + args { + ...InputValue + } + } + } +} + +fragment FullType on __Type { + kind + name + description + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } +} + +fragment InputValue on __InputValue { + name + description + type { + ...TypeRef + } + defaultValue +} + +fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } +} +` + +type stringSliceFlag []string + +func (v *stringSliceFlag) String() string { + return fmt.Sprint([]string(*v)) +} + +func (v *stringSliceFlag) Set(s string) error { + *v = append(*v, s) + return nil +} + +type transport struct { + http.RoundTripper + + header http.Header +} + +func (tr *transport) RoundTrip(req *http.Request) (*http.Response, error) { + for k, values := range tr.header { + for _, v := range values { + req.Header.Add(k, v) + } + } + return tr.RoundTripper.RoundTrip(req) +} + +func typeKeyword(t Type) string { + switch t.Kind { + case TypeKindObject: + return "type" + case TypeKindInterface: + return "interface" + default: + panic("unreachable") + } +} + +func typeReference(t Type) string { + var modifiers []TypeKind + + ofType := &t + for ofType.OfType != nil { + switch ofType.Kind { + case TypeKindList, TypeKindNonNull: + modifiers = append(modifiers, ofType.Kind) + default: + panic("invalid type") + } + ofType = ofType.OfType + } + + if ofType.Name == nil { + return "<invalid>" + } + typeName := *ofType.Name + if len(modifiers) > 0 { + for i := len(modifiers) - 1; i >= 0; i-- { + switch modifiers[i] { + case TypeKindList: + typeName = "[" + typeName + "]" + case TypeKindNonNull: + typeName += "!" + } + } + } + return typeName +} + +func printDescription(desc string, prefix string) { + if !strings.Contains(desc, "\n") { + fmt.Printf("%s%s\n", prefix, termfmt.Dim.Sprintf("\"%s\"", desc)) + } else { + fmt.Printf("%s%s\n", prefix, termfmt.Dim.Sprintf("\"\"\"")) + for _, line := range strings.Split(desc, "\n") { + fmt.Printf("%s%s\n", prefix, termfmt.Dim.Sprint(line)) + } + fmt.Printf("%s%s\n", prefix, termfmt.Dim.Sprintf("\"\"\"")) + } +} + +func printType(t Type) { + if t.Description != nil && *t.Description != "" { + printDescription(*t.Description, "") + } + switch t.Kind { + case TypeKindScalar: + fmt.Printf("scalar %s\n\n", termfmt.Bold.Sprintf(*t.Name)) + case TypeKindUnion: + fmt.Printf("union %s = ", termfmt.Bold.Sprintf(*t.Name)) + for idx, i := range t.Interfaces { + if idx > 0 { + fmt.Print(" | ") + } + fmt.Printf("%s", typeReference(i)) + } + fmt.Print("\n\n") + case TypeKindEnum: + fmt.Printf("enum %s {\n", termfmt.Bold.Sprintf(*t.Name)) + for _, e := range t.EnumValues { + if e.Description != nil && *e.Description != "" { + printDescription(*e.Description, "\t") + } + fmt.Printf(" %s\n", e.Name) + } + fmt.Print("}\n\n") + case TypeKindInputObject: + fmt.Printf("input %s {\n", termfmt.Bold.Sprintf(*t.Name)) + for _, f := range t.InputFields { + if f.Description != nil && *f.Description != "" { + printDescription(*f.Description, "\t") + } + fmt.Printf(" %s: %s", f.Name, typeReference(*f.Type)) + if f.DefaultValue != nil { + fmt.Printf(" = %s", *f.DefaultValue) + } + fmt.Println() + } + fmt.Print("}\n\n") + case TypeKindObject, TypeKindInterface: + fmt.Printf("%s %s {\n", typeKeyword(t), termfmt.Bold.Sprintf(*t.Name)) + for _, f := range t.Fields { + if f.Description != nil && *f.Description != "" { + printDescription(*f.Description, "\t") + } + fmt.Printf(" %s", f.Name) + if len(f.Args) > 0 { + fmt.Print("(") + for idx, a := range f.Args { + if idx > 0 { + fmt.Print(", ") + } + fmt.Printf("%s: %s", a.Name, typeReference(*a.Type)) + if a.DefaultValue != nil { + fmt.Printf(" = %s", *a.DefaultValue) + } + } + fmt.Print(")") + } + fmt.Printf(": %s\n", typeReference(*f.Type)) + } + fmt.Print("}\n\n") + } +} + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + var header []string + flag.Var((*stringSliceFlag)(&header), "H", "set HTTP header") + flag.Parse() + + endpoint := flag.Arg(0) + if endpoint == "" { + log.Fatalf("missing endpoint") + } + + op := gqlclient.NewOperation(query) + + tr := transport{ + RoundTripper: http.DefaultTransport, + header: make(http.Header), + } + httpClient := http.Client{Transport: &tr} + gqlClient := gqlclient.New(endpoint, &httpClient) + + for _, kv := range header { + parts := strings.SplitN(kv, ":", 2) + if len(parts) != 2 { + log.Fatalf("in header definition %q: missing colon", kv) + } + tr.header.Add(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])) + } + + var data struct { + Schema Schema `json:"__schema"` + } + if err := gqlClient.Execute(ctx, op, &data); err != nil { + log.Fatal(err) + } + + for _, t := range data.Schema.Types { + if t.Name != nil && strings.HasPrefix(*t.Name, "__") { + continue + } + printType(t) + } +} diff --git a/cmd/gqlintrospect/types.go b/cmd/gqlintrospect/types.go new file mode 100644 index 0000000..f977ae3 --- /dev/null +++ b/cmd/gqlintrospect/types.go @@ -0,0 +1,89 @@ +package main + +type Directive struct { + Name string `json:"name"` + Description *string `json:"description,omitempty"` + Locations []DirectiveLocation `json:"locations"` + Args []InputValue `json:"args"` + IsRepeatable bool `json:"isRepeatable"` +} + +type DirectiveLocation string + +const ( + DirectiveLocationQuery DirectiveLocation = "QUERY" + DirectiveLocationMutation DirectiveLocation = "MUTATION" + DirectiveLocationSubscription DirectiveLocation = "SUBSCRIPTION" + DirectiveLocationField DirectiveLocation = "FIELD" + DirectiveLocationFragmentDefinition DirectiveLocation = "FRAGMENT_DEFINITION" + DirectiveLocationFragmentSpread DirectiveLocation = "FRAGMENT_SPREAD" + DirectiveLocationInlineFragment DirectiveLocation = "INLINE_FRAGMENT" + DirectiveLocationVariableDefinition DirectiveLocation = "VARIABLE_DEFINITION" + DirectiveLocationSchema DirectiveLocation = "SCHEMA" + DirectiveLocationScalar DirectiveLocation = "SCALAR" + DirectiveLocationObject DirectiveLocation = "OBJECT" + DirectiveLocationFieldDefinition DirectiveLocation = "FIELD_DEFINITION" + DirectiveLocationArgumentDefinition DirectiveLocation = "ARGUMENT_DEFINITION" + DirectiveLocationInterface DirectiveLocation = "INTERFACE" + DirectiveLocationUnion DirectiveLocation = "UNION" + DirectiveLocationEnum DirectiveLocation = "ENUM" + DirectiveLocationEnumValue DirectiveLocation = "ENUM_VALUE" + DirectiveLocationInputObject DirectiveLocation = "INPUT_OBJECT" + DirectiveLocationInputFieldDefinition DirectiveLocation = "INPUT_FIELD_DEFINITION" +) + +type EnumValue struct { + Name string `json:"name"` + Description *string `json:"description,omitempty"` + IsDeprecated bool `json:"isDeprecated"` + DeprecationReason *string `json:"deprecationReason,omitempty"` +} + +type Field struct { + Name string `json:"name"` + Description *string `json:"description,omitempty"` + Args []InputValue `json:"args"` + Type *Type `json:"type"` + IsDeprecated bool `json:"isDeprecated"` + DeprecationReason *string `json:"deprecationReason,omitempty"` +} + +type InputValue struct { + Name string `json:"name"` + Description *string `json:"description,omitempty"` + Type *Type `json:"type"` + DefaultValue *string `json:"defaultValue,omitempty"` +} + +type Schema struct { + Types []Type `json:"types"` + QueryType *Type `json:"queryType"` + MutationType *Type `json:"mutationType,omitempty"` + SubscriptionType *Type `json:"subscriptionType,omitempty"` + Directives []Directive `json:"directives"` +} + +type Type struct { + Kind TypeKind `json:"kind"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Fields []Field `json:"fields,omitempty"` + Interfaces []Type `json:"interfaces,omitempty"` + PossibleTypes []Type `json:"possibleTypes,omitempty"` + EnumValues []EnumValue `json:"enumValues,omitempty"` + InputFields []InputValue `json:"inputFields,omitempty"` + OfType *Type `json:"ofType,omitempty"` +} + +type TypeKind string + +const ( + TypeKindScalar TypeKind = "SCALAR" + TypeKindObject TypeKind = "OBJECT" + TypeKindInterface TypeKind = "INTERFACE" + TypeKindUnion TypeKind = "UNION" + TypeKindEnum TypeKind = "ENUM" + TypeKindInputObject TypeKind = "INPUT_OBJECT" + TypeKindList TypeKind = "LIST" + TypeKindNonNull TypeKind = "NON_NULL" +) diff --git a/go.mod b/go.mod index 8e0f4f9..6affc20 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,10 @@ module git.sr.ht/~emersion/gqlclient go 1.17 require ( + git.sr.ht/~emersion/hut v0.1.0 // indirect github.com/agnivade/levenshtein v1.0.1 // indirect github.com/dave/jennifer v1.4.1 // indirect github.com/vektah/gqlparser/v2 v2.2.0 // indirect + golang.org/x/sys v0.0.0-20220329152356-43be30ef3008 // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect ) diff --git a/go.sum b/go.sum index 55afd40..e6d373f 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +git.sr.ht/~emersion/hut v0.1.0 h1:13g2eHiUPlfrX3Xw05PIphE5KvXq+5QmAlqbSKoT630= +git.sr.ht/~emersion/hut v0.1.0/go.mod h1:6Ls/fK4tZk9XTbAL3HmFe75J0Df84My7QogKvW5Be0k= 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/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= @@ -14,6 +16,10 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/vektah/gqlparser/v2 v2.2.0 h1:bAc3slekAAJW6sZTi07aGq0OrfaCjj4jxARAaC7g2EM= github.com/vektah/gqlparser/v2 v2.2.0/go.mod h1:i3mQIGIrbK2PD1RrCeMTlVbkF2FJ6WkU1KJlJlC+3F4= +golang.org/x/sys v0.0.0-20220329152356-43be30ef3008 h1:pq9pwoi2rjLWvmiVser/lIOgiyA3fli4M+RfGVMA7nE= +golang.org/x/sys v0.0.0-20220329152356-43be30ef3008/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -- 2.37.0