1
2
3 package main
4
5 import (
6 "bytes"
7 "encoding/json"
8 "flag"
9 "fmt"
10 "go/format"
11 "io"
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
23 func main() {
24 flag.Parse()
25
26 err := run()
27 if err != nil {
28 log.Fatalln(err)
29 }
30 }
31
32 func 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 = os.WriteFile(filename, out, 0644)
55 if err != nil {
56 return err
57 }
58 }
59
60 return nil
61 }
62
63 func 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 if resp.StatusCode != http.StatusOK {
75 body, _ := io.ReadAll(resp.Body)
76 return nil, fmt.Errorf("non-200 OK status code: %v body: %q", resp.Status, body)
77 }
78 err = json.NewDecoder(resp.Body).Decode(&schema)
79 return schema, err
80 }
81
82
83 var templates = map[string]*template.Template{
84 "enum.go": t(`// Code generated by gen.go; DO NOT EDIT.
85
86 package githubv4
87 {{range .data.__schema.types | sortByName}}{{if and (eq .kind "ENUM") (not (internal .name))}}
88 {{template "enum" .}}
89 {{end}}{{end}}
90
91
92 {{- define "enum" -}}
93 // {{.name}} {{.description | clean | endSentence}}
94 type {{.name}} string
95
96 // {{.description | clean | fullSentence}}
97 const ({{range .enumValues}}
98 {{enumIdentifier $.name .name}} {{$.name}} = {{.name | quote}} // {{.description | clean | fullSentence}}{{end}}
99 )
100 {{- end -}}
101 `),
102
103 "input.go": t(`// Code generated by gen.go; DO NOT EDIT.
104
105 package githubv4
106
107 // Input represents one of the Input structs:
108 //
109 // {{join (inputObjects .data.__schema.types) ", "}}.
110 type Input interface{}
111 {{range .data.__schema.types | sortByName}}{{if eq .kind "INPUT_OBJECT"}}
112 {{template "inputObject" .}}
113 {{end}}{{end}}
114
115
116 {{- define "inputObject" -}}
117 // {{.name}} {{.description | clean | endSentence}}
118 type {{.name}} struct {{"{"}}{{range .inputFields}}{{if eq .type.kind "NON_NULL"}}
119 // {{.description | clean | fullSentence}} (Required.)
120 {{.name | identifier}} {{.type | type}} ` + "`" + `json:"{{.name}}"` + "`" + `{{end}}{{end}}
121 {{range .inputFields}}{{if ne .type.kind "NON_NULL"}}
122 // {{.description | clean | fullSentence}} (Optional.)
123 {{.name | identifier}} {{.type | type}} ` + "`" + `json:"{{.name}},omitempty"` + "`" + `{{end}}{{end}}
124 }
125 {{- end -}}
126 `),
127 }
128
129 func t(text string) *template.Template {
130
131 var typeString func(t map[string]interface{}) string
132 typeString = func(t map[string]interface{}) string {
133 switch t["kind"] {
134 case "NON_NULL":
135 s := typeString(t["ofType"].(map[string]interface{}))
136 if !strings.HasPrefix(s, "*") {
137 panic(fmt.Errorf("nullable type %q doesn't begin with '*'", s))
138 }
139 return s[1:]
140 case "LIST":
141 return "*[]" + typeString(t["ofType"].(map[string]interface{}))
142 default:
143 return "*" + t["name"].(string)
144 }
145 }
146
147 return template.Must(template.New("").Funcs(template.FuncMap{
148 "internal": func(s string) bool { return strings.HasPrefix(s, "__") },
149 "quote": strconv.Quote,
150 "join": strings.Join,
151 "sortByName": func(types []interface{}) []interface{} {
152 sort.Slice(types, func(i, j int) bool {
153 ni := types[i].(map[string]interface{})["name"].(string)
154 nj := types[j].(map[string]interface{})["name"].(string)
155 return ni < nj
156 })
157 return types
158 },
159 "inputObjects": func(types []interface{}) []string {
160 var names []string
161 for _, t := range types {
162 t := t.(map[string]interface{})
163 if t["kind"].(string) != "INPUT_OBJECT" {
164 continue
165 }
166 names = append(names, t["name"].(string))
167 }
168 sort.Strings(names)
169 return names
170 },
171 "identifier": func(name string) string { return ident.ParseLowerCamelCase(name).ToMixedCaps() },
172 "enumIdentifier": func(enum, value string) string {
173 var brandNames = map[string]string{
174 "LINKEDIN": "LinkedIn",
175 "YOUTUBE": "YouTube",
176 }
177 switch brand, isBrand := brandNames[value]; {
178 case enum == "SponsorsCountryOrRegionCode":
179
180
181 return enum + value
182 case value == "SCIM":
183
184
185 return enum + value
186 case isBrand:
187
188 return enum + brand
189 default:
190 return enum + ident.ParseScreamingSnakeCase(value).ToMixedCaps()
191 }
192 },
193 "type": typeString,
194 "clean": func(s string) string { return strings.Join(strings.Fields(s), " ") },
195 "endSentence": func(s string) string {
196 s = strings.ToLower(s[0:1]) + s[1:]
197 switch {
198 default:
199 s = "represents " + s
200 case strings.HasPrefix(s, "autogenerated "):
201 s = "is an " + s
202 case strings.HasPrefix(s, "specifies "):
203
204 }
205 if !strings.HasSuffix(s, ".") {
206 s += "."
207 }
208 return s
209 },
210 "fullSentence": func(s string) string {
211 if !strings.HasSuffix(s, ".") {
212 s += "."
213 }
214 return s
215 },
216 }).Parse(text))
217 }
218
View as plain text