Skip to content

Commit

Permalink
Merge pull request #285 from OpsLevel/db/generate-api-input-objects
Browse files Browse the repository at this point in the history
generate api input objects with "task gen"
  • Loading branch information
davidbloss authored Oct 25, 2023
2 parents 8ec23ef + 22587bb commit f0942d7
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 106 deletions.
1 change: 1 addition & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ version: '3'
tasks:
generate:
desc: Generate code based on public GraphQL Interface
aliases: [gen]
cmds:
- go generate
- gofumpt -w .
Expand Down
274 changes: 168 additions & 106 deletions gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,58 @@ import (
"flag"
"fmt"
"go/format"
"io/ioutil"
"log"
"os"
"sort"
"strconv"
"strings"
"text/template"
"unicode"

"github.com/Masterminds/sprig/v3"
"github.com/hasura/go-graphql-client/ident"
"github.com/opslevel/opslevel-go/v2023"
)

const (
enumFile string = "enum.go"
inputObjectFile string = "input.go"
)

type GraphQLSchema struct {
Types []GraphQLTypes `graphql:"types" json:"types"`
}

type IntrospectiveType struct {
Name string `graphql:"name" json:"name"`
Kind string `graphql:"kind" json:"kind"`
OfType struct {
OfTypeName string `graphql:"name" json:"name"`
} `graphql:"ofType" json:"ofType"`
}

type GraphQLInputValue struct {
Name string `graphql:"name" json:"name"`
DefaultValue string `graphql:"defaultValue" json:"defaultValue"`
Description string `graphql:"description" json:"description"`
Type IntrospectiveType `graphql:"type" json:"type"`
}

type GraphQLField struct {
Args []GraphQLInputValue `graphql:"args" json:"args"`
Description string `graphql:"description" json:"description"`
IsDeprecated bool `graphql:"isDeprecated" json:"isDeprecated"`
Name string `graphql:"name" json:"name"`
}

type GraphQLTypes struct {
Name string `graphql:"name" json:"name"`
Kind string `graphql:"kind" json:"kind"`
Description string `graphql:"description" json:"description"`
PossibleTypes []GraphQLPossibleType `graphql:"possibleTypes"`
// Fields ?
// InputFields ?
EnumValues []GraphQLEnumValues `graphql:"enumValues" json:"enumValues"`
EnumValues []GraphQLEnumValues `graphql:"enumValues" json:"enumValues"`
Fields []GraphQLField `graphql:"fields" json:"fields"`
InputFields []GraphQLInputValue `graphql:"inputFields" json:"inputFields"`
}

type GraphQLEnumValues struct {
Expand Down Expand Up @@ -69,20 +97,62 @@ func main() {
}
}

func run() error {
func getRootSchema() (*GraphQLSchema, error) {
token, ok := os.LookupEnv("OPSLEVEL_API_TOKEN")
if !ok {
return fmt.Errorf("OPSLEVEL_API_TOKEN environment variable not set")
return nil, fmt.Errorf("OPSLEVEL_API_TOKEN environment variable not set")
}
client := opslevel.NewClient(token, opslevel.SetAPIVisibility("public"))
client := opslevel.NewGQLClient(opslevel.SetAPIToken(token), opslevel.SetAPIVisibility("public"))
schema, err := GetSchema(client)
if err != nil {
return nil, err
}
return schema, nil
}

func run() error {
schema, err := getRootSchema()
if err != nil {
return err
}

enumSchema := GraphQLSchema{}
inputObjectSchema := GraphQLSchema{}
interfaceSchema := GraphQLSchema{}
objectSchema := GraphQLSchema{}
scalarSchema := GraphQLSchema{}
unionSchema := GraphQLSchema{}
for _, t := range schema.Types {
switch t.Kind {
case "ENUM":
enumSchema.Types = append(enumSchema.Types, t)
case "SCALAR":
scalarSchema.Types = append(scalarSchema.Types, t)
case "INTERFACE":
interfaceSchema.Types = append(interfaceSchema.Types, t)
case "INPUT_OBJECT":
inputObjectSchema.Types = append(inputObjectSchema.Types, t)
case "OBJECT":
objectSchema.Types = append(objectSchema.Types, t)
case "UNION":
unionSchema.Types = append(unionSchema.Types, t)
default:
panic("Unknown GraphQL type: " + t.Kind)
}
}

var buf bytes.Buffer
var subSchema GraphQLSchema
for filename, t := range templates {
var buf bytes.Buffer
err := t.Execute(&buf, schema)
switch filename {
case enumFile:
subSchema = enumSchema
case inputObjectFile:
subSchema = inputObjectSchema
default:
panic("Unknown file: " + filename)
}
err := t.Execute(&buf, subSchema)
if err != nil {
return err
}
Expand All @@ -91,8 +161,9 @@ func run() error {
log.Println(err)
out = []byte("// gofmt error: " + err.Error() + "\n\n" + buf.String())
}
buf.Reset()
fmt.Println("writing", filename)
err = ioutil.WriteFile(filename, out, 0o644)
err = os.WriteFile(filename, out, 0o644)
if err != nil {
return err
}
Expand All @@ -103,7 +174,7 @@ func run() error {

// Filename -> Template.
var templates = map[string]*template.Template{
"enum.go": t(`// Code generated by gen.go; DO NOT EDIT.
enumFile: t(`// Code generated by gen.go; DO NOT EDIT.
package opslevel
{{range .Types | sortByName}}{{if and (eq .Kind "ENUM") (not (internal .Name))}}
Expand All @@ -125,48 +196,22 @@ var All{{$.Name}} = []string {
}
{{- end -}}
`),
/*
"input.go": t(`// Code generated by gen.go; DO NOT EDIT.
package opslevel
type Input interface{}
{{range .Types | sortByName}}{{if eq .Kind "INPUT_OBJECT"}}
{{template "inputObject" .}}
{{end}}{{end}}
{{- define "inputObject" -}}
// {{.Name}} {{.Description | clean | endSentence}}
type {{.Name}} struct {}
{{- end -}}
`),
*/
// TODO: fix this to generate all Input structs
// "input.go": t(`// Code generated by gen.go; DO NOT EDIT.

// package opslevel

// // Input represents one of the Input structs:
// //
// // {{join (inputObjects .data.__schema.types) ", "}}.
// type Input interface{}
// {{range .data.__schema.types | sortByName}}{{if eq .kind "INPUT_OBJECT"}}
// {{template "inputObject" .}}
// {{end}}{{end}}

// {{- define "inputObject" -}}
// // {{.name}} {{.description | clean | endSentence}}
// type {{.name}} struct {{"{"}}{{range .inputFields}}{{if eq .type.kind "NON_NULL"}}
// // {{.description | clean | fullSentence}} (Required.)
// {{.name | identifier}} {{.type | type}} ` + "`" + `json:"{{.name}}"` + "`" + `{{end}}{{end}}
// {{range .inputFields}}{{if ne .type.kind "NON_NULL"}}
// // {{.description | clean | fullSentence}} (Optional.)
// {{.name | identifier}} {{.type | type}} ` + "`" + `json:"{{.name}},omitempty"` + "`" + `{{end}}{{end}}
// }
// {{- end -}}
// `),
inputObjectFile: t(`// Code generated by gen.go; DO NOT EDIT.
package opslevel
{{range .Types | sortByName}}{{if and (eq .Kind "INPUT_OBJECT") (not (internal .Name))}}
{{template "input_object" .}}
{{end}}{{end}}
{{- define "input_object" -}}
// {{.Name}} {{.Description | clean | endSentence}}
type {{.Name}} struct { {{range .InputFields }}
// {{.Description | clean | fullSentence}} {{if eq .Type.Kind "NON_NULL"}}(Required.){{else}}(Optional.){{end}}
{{.Name | title}} {{.Type.OfType.OfTypeName | lowerStringType}} ` + "`" + `json:"{{.Name | lowerFirst }}{{if ne .Type.Kind "NON_NULL"}},omitempty{{end}}"` +
"`" + `{{end}}
}
{{- end -}}
`),
}

func t(text string) *template.Template {
Expand All @@ -187,59 +232,76 @@ func t(text string) *template.Template {
}
}

return template.Must(template.New("").Funcs(template.FuncMap{
"internal": func(s string) bool { return strings.HasPrefix(s, "__") },
"quote": strconv.Quote,
"join": strings.Join,
"sortByName": func(types []GraphQLTypes) []GraphQLTypes {
sort.Slice(types, func(i, j int) bool {
ni := types[i].Name
nj := types[j].Name
return ni < nj
})
return types
},
"inputObjects": func(types []interface{}) []string {
var names []string
for _, t := range types {
t := t.(map[string]interface{})
if t["kind"].(string) != "INPUT_OBJECT" {
continue
}
names = append(names, t["name"].(string))
}
sort.Strings(names)
return names
},
"identifier": func(name string) string { return ident.ParseLowerCamelCase(name).ToMixedCaps() },
"enumIdentifier": func(name string) string { return ident.ParseScreamingSnakeCase(name).ToMixedCaps() },
"type": typeString,
"clean": func(s string) string { return strings.Join(strings.Fields(s), " ") },
"endSentence": func(s string) string {
if len(s) == 0 {
// Do nothing.
return ""
}
genTemplate := template.New("")
genTemplate.Funcs(templFuncMap)
genTemplate.Funcs(sprig.TxtFuncMap())
genTemplate.Funcs(template.FuncMap{"type": typeString})
return template.Must(genTemplate.Parse(text))
}

s = strings.ToLower(s[0:1]) + s[1:]
switch {
default:
s = "represents " + s
case strings.HasPrefix(s, "autogenerated "):
s = "is an " + s
case strings.HasPrefix(s, "specifies "):
// Do nothing.
}
if !strings.HasSuffix(s, ".") {
s += "."
}
return s
},
"fullSentence": func(s string) string {
if !strings.HasSuffix(s, ".") {
s += "."
var templFuncMap = template.FuncMap{
"internal": func(s string) bool { return strings.HasPrefix(s, "__") },
"quote": strconv.Quote,
"join": strings.Join,
"lowerStringType": func(value string) string {
if value == "String" || value == "" {
return "string"
}
return value
},
"lowerFirst": func(value string) string {
for i, v := range value {
return string(unicode.ToLower(v)) + value[i+1:]
}
return value
},
"sortByName": func(types []GraphQLTypes) []GraphQLTypes {
sort.Slice(types, func(i, j int) bool {
ni := types[i].Name
nj := types[j].Name
return ni < nj
})
return types
},
"inputObjects": func(types []interface{}) []string {
var names []string
for _, t := range types {
t := t.(map[string]interface{})
if t["kind"].(string) != "INPUT_OBJECT" {
continue
}
return s
},
}).Parse(text))
names = append(names, t["name"].(string))
}
sort.Strings(names)
return names
},
"identifier": func(name string) string { return ident.ParseLowerCamelCase(name).ToMixedCaps() },
"enumIdentifier": func(name string) string { return ident.ParseScreamingSnakeCase(name).ToMixedCaps() },
"clean": func(s string) string { return strings.Join(strings.Fields(s), " ") },
"endSentence": func(s string) string {
if len(s) == 0 {
// Do nothing.
return ""
}

s = strings.ToLower(s[0:1]) + s[1:]
switch {
default:
s = "represents " + s
case strings.HasPrefix(s, "autogenerated "):
s = "is an " + s
case strings.HasPrefix(s, "specifies "):
// Do nothing.
}
if !strings.HasSuffix(s, ".") {
s += "."
}
return s
},
"fullSentence": func(s string) string {
if !strings.HasSuffix(s, ".") {
s += "."
}
return s
},
}

0 comments on commit f0942d7

Please sign in to comment.