gen.go

  1// +build ignore
  2
  3package main
  4
  5import (
  6	"bytes"
  7	"encoding/json"
  8	"flag"
  9	"fmt"
 10	"go/format"
 11	"io/ioutil"
 12	"log"
 13	"net/http"
 14	"os"
 15	"sort"
 16	"strconv"
 17	"strings"
 18	"text/template"
 19
 20	"github.com/shurcooL/graphql/ident"
 21)
 22
 23func main() {
 24	flag.Parse()
 25
 26	err := run()
 27	if err != nil {
 28		log.Fatalln(err)
 29	}
 30}
 31
 32func run() error {
 33	githubToken, ok := os.LookupEnv("GITHUB_TOKEN")
 34	if !ok {
 35		return fmt.Errorf("GITHUB_TOKEN environment variable not set")
 36	}
 37	schema, err := loadSchema(githubToken)
 38	if err != nil {
 39		return err
 40	}
 41
 42	for filename, t := range templates {
 43		var buf bytes.Buffer
 44		err := t.Execute(&buf, schema)
 45		if err != nil {
 46			return err
 47		}
 48		out, err := format.Source(buf.Bytes())
 49		if err != nil {
 50			log.Println(err)
 51			out = []byte("// gofmt error: " + err.Error() + "\n\n" + buf.String())
 52		}
 53		fmt.Println("writing", filename)
 54		err = ioutil.WriteFile(filename, out, 0644)
 55		if err != nil {
 56			return err
 57		}
 58	}
 59
 60	return nil
 61}
 62
 63func loadSchema(githubToken string) (schema interface{}, err error) {
 64	req, err := http.NewRequest("GET", "https://api.github.com/graphql", nil)
 65	if err != nil {
 66		return nil, err
 67	}
 68	req.Header.Set("Authorization", "bearer "+githubToken)
 69	resp, err := http.DefaultClient.Do(req)
 70	if err != nil {
 71		return nil, err
 72	}
 73	defer resp.Body.Close()
 74	err = json.NewDecoder(resp.Body).Decode(&schema)
 75	return schema, err
 76}
 77
 78// Filename -> Template.
 79var templates = map[string]*template.Template{
 80	"enum.go": t(`// Code generated by gen.go; DO NOT EDIT.
 81
 82package githubv4
 83{{range .data.__schema.types | sortByName}}{{if and (eq .kind "ENUM") (not (internal .name))}}
 84{{template "enum" .}}
 85{{end}}{{end}}
 86
 87
 88{{- define "enum" -}}
 89// {{.name}} {{.description | endSentence}}
 90type {{.name}} string
 91
 92// {{.description | fullSentence}}
 93const ({{range .enumValues}}
 94	{{$.name}}{{.name | enumIdentifier}} {{$.name}} = {{.name | quote}} // {{.description | fullSentence}}{{end}}
 95)
 96{{- end -}}
 97`),
 98
 99	"input.go": t(`// Code generated by gen.go; DO NOT EDIT.
100
101package githubv4
102
103// Input represents one of the Input structs:
104//
105// {{join (inputObjects .data.__schema.types) ", "}}.
106type Input interface{}
107{{range .data.__schema.types | sortByName}}{{if eq .kind "INPUT_OBJECT"}}
108{{template "inputObject" .}}
109{{end}}{{end}}
110
111
112{{- define "inputObject" -}}
113// {{.name}} {{.description | endSentence}}
114type {{.name}} struct {{"{"}}{{range .inputFields}}{{if eq .type.kind "NON_NULL"}}
115	// {{.description | fullSentence}} (Required.)
116	{{.name | identifier}} {{.type | type}} ` + "`" + `json:"{{.name}}"` + "`" + `{{end}}{{end}}
117{{range .inputFields}}{{if ne .type.kind "NON_NULL"}}
118	// {{.description | fullSentence}} (Optional.)
119	{{.name | identifier}} {{.type | type}} ` + "`" + `json:"{{.name}},omitempty"` + "`" + `{{end}}{{end}}
120}
121{{- end -}}
122`),
123}
124
125func t(text string) *template.Template {
126	// typeString returns a string representation of GraphQL type t.
127	var typeString func(t map[string]interface{}) string
128	typeString = func(t map[string]interface{}) string {
129		switch t["kind"] {
130		case "NON_NULL":
131			s := typeString(t["ofType"].(map[string]interface{}))
132			if !strings.HasPrefix(s, "*") {
133				panic(fmt.Errorf("nullable type %q doesn't begin with '*'", s))
134			}
135			return s[1:] // Strip star from nullable type to make it non-null.
136		case "LIST":
137			return "*[]" + typeString(t["ofType"].(map[string]interface{}))
138		default:
139			return "*" + t["name"].(string)
140		}
141	}
142
143	return template.Must(template.New("").Funcs(template.FuncMap{
144		"internal": func(s string) bool { return strings.HasPrefix(s, "__") },
145		"quote":    strconv.Quote,
146		"join":     strings.Join,
147		"sortByName": func(types []interface{}) []interface{} {
148			sort.Slice(types, func(i, j int) bool {
149				ni := types[i].(map[string]interface{})["name"].(string)
150				nj := types[j].(map[string]interface{})["name"].(string)
151				return ni < nj
152			})
153			return types
154		},
155		"inputObjects": func(types []interface{}) []string {
156			var names []string
157			for _, t := range types {
158				t := t.(map[string]interface{})
159				if t["kind"].(string) != "INPUT_OBJECT" {
160					continue
161				}
162				names = append(names, t["name"].(string))
163			}
164			sort.Strings(names)
165			return names
166		},
167		"identifier":     func(name string) string { return ident.ParseLowerCamelCase(name).ToMixedCaps() },
168		"enumIdentifier": func(name string) string { return ident.ParseScreamingSnakeCase(name).ToMixedCaps() },
169		"type":           typeString,
170		"endSentence": func(s string) string {
171			s = strings.ToLower(s[0:1]) + s[1:]
172			switch {
173			default:
174				s = "represents " + s
175			case strings.HasPrefix(s, "autogenerated "):
176				s = "is an " + s
177			case strings.HasPrefix(s, "specifies "):
178				// Do nothing.
179			}
180			if !strings.HasSuffix(s, ".") {
181				s += "."
182			}
183			return s
184		},
185		"fullSentence": func(s string) string {
186			if !strings.HasSuffix(s, ".") {
187				s += "."
188			}
189			return s
190		},
191	}).Parse(text))
192}