---
v5: properly escape descriptions, fix union member printing, print
implemented interfaces
cmd/gqlintrospect/main.go | 317 +++++++++++++++++++++++++++++++++++++
cmd/gqlintrospect/types.go | 89 +++++++++++
2 files changed, 406 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..e5b515a
--- /dev/null
+++ b/cmd/gqlintrospect/main.go
@@ -0,0 +1,317 @@
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "log"
+ "net/http"
+ "strings"
+ "time"
+
+ "git.sr.ht/~emersion/gqlclient"
+)
+
+// The query used 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) {
+ desc = strings.Replace(desc, `\`, `\\`, -1)
+ desc = strings.Replace(desc, `"`, `\"`, -1)
+ if !strings.Contains(desc, "\n") {
+ fmt.Printf("%s\"%s\"\n", prefix, desc)
+ } else {
+ fmt.Printf("%s\"\"\"\n", prefix)
+ for _, line := range strings.Split(desc, "\n") {
+ fmt.Printf("%s%s\n", prefix, line)
+ }
+ fmt.Printf("%s\"\"\"\n", prefix)
+ }
+}
+
+func printType(t Type) {
+ if t.Description != nil && *t.Description != "" {
+ printDescription(*t.Description, "")
+ }
+ switch t.Kind {
+ case TypeKindScalar:
+ fmt.Printf("scalar %s\n\n", *t.Name)
+ case TypeKindUnion:
+ fmt.Printf("union %s = ", *t.Name)
+ for idx, i := range t.PossibleTypes {
+ if idx > 0 {
+ fmt.Print(" | ")
+ }
+ fmt.Printf("%s", typeReference(i))
+ }
+ fmt.Print("\n\n")
+ case TypeKindEnum:
+ fmt.Printf("enum %s {\n", *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", *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", typeKeyword(t), *t.Name)
+ if len(t.Interfaces) > 0 {
+ fmt.Printf(" implements ")
+ for idx, i := range t.Interfaces {
+ if idx > 0 {
+ fmt.Print(", ")
+ }
+ fmt.Printf(typeReference(i))
+ }
+ }
+ fmt.Print(" {\n")
+ 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"
+)
--
2.37.0