~emersion/public-inbox

gqlclient: cmd/gqlintrospect: new command v2 SUPERSEDED

Adnan Maolood: 1
 cmd/gqlintrospect: new command

 3 files changed, 394 insertions(+), 0 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~emersion/public-inbox/patches/33797/mbox | git am -3
Learn more about email & git

[PATCH gqlclient v2] cmd/gqlintrospect: new command Export this patch

---
v2: use printDescription to print enum value descriptions as well
 cmd/gqlintrospect/main.go | 385 ++++++++++++++++++++++++++++++++++++++
 go.mod                    |   3 +
 go.sum                    |   6 +
 3 files changed, 394 insertions(+)
 create mode 100644 cmd/gqlintrospect/main.go

diff --git a/cmd/gqlintrospect/main.go b/cmd/gqlintrospect/main.go
new file mode 100644
index 0000000..860c1cc
--- /dev/null
+++ b/cmd/gqlintrospect/main.go
@@ -0,0 +1,385 @@
package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"net/http"
	"strings"
	"time"

	"git.sr.ht/~emersion/gqlclient"
	"git.sr.ht/~emersion/hut/termfmt"
)

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"
)

// The query generated by graphiql to determine type information
const query = `
query IntrospectionQuery {
  __schema {
    description
    queryType {
      name
    }
    mutationType {
      name
    }
    subscriptionType {
      name
    }
    types {
      ...FullType
    }
    directives {
      name
      description
      locations
      args {
        ...InputValue
      }
    }
  }
}

fragment FullType on __Type {
  kind
  name
  description
  specifiedByURL
  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"
	case TypeKindInputObject:
		return "input"
	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 TypeKindObject, TypeKindInterface, TypeKindInputObject:
		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/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