...

Source file src/github.com/shurcooL/githubv4/gen.go

Documentation: github.com/shurcooL/githubv4

     1  //go:build ignore
     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  // Filename -> Template.
    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  	// typeString returns a string representation of GraphQL type t.
   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:] // Strip star from nullable type to make it non-null.
   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{ // Augments the list in ident.
   174  				"LINKEDIN": "LinkedIn",
   175  				"YOUTUBE":  "YouTube",
   176  			}
   177  			switch brand, isBrand := brandNames[value]; {
   178  			case enum == "SponsorsCountryOrRegionCode":
   179  				// These enum values are country/region codes like "CA" or "US"
   180  				// rather than words, so don't try to convert them to mixed caps.
   181  				return enum + value
   182  			case value == "SCIM":
   183  				// An initialism or acronym that ident doesn't know about.
   184  				// Use it as is to preserve its consistent case (https://go.dev/s/style#initialisms).
   185  				return enum + value
   186  			case isBrand:
   187  				// Brand name that ident doesn't know about.
   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  				// Do nothing.
   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