403 lines
10 KiB
Go
403 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"cmp"
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
"text/template"
|
|
|
|
core "dappco.re/go/core"
|
|
coreio "dappco.re/go/core/io"
|
|
)
|
|
|
|
// typeGrouping maps type name prefixes to output file names.
|
|
var typeGrouping = map[string]string{
|
|
"Repository": "repo",
|
|
"Repo": "repo",
|
|
"Issue": "issue",
|
|
"PullRequest": "pr",
|
|
"Pull": "pr",
|
|
"User": "user",
|
|
"Organization": "org",
|
|
"Org": "org",
|
|
"Team": "team",
|
|
"Label": "label",
|
|
"Milestone": "milestone",
|
|
"Release": "release",
|
|
"Tag": "tag",
|
|
"Branch": "branch",
|
|
"Hook": "hook",
|
|
"Deploy": "key",
|
|
"PublicKey": "key",
|
|
"GPGKey": "key",
|
|
"Key": "key",
|
|
"Notification": "notification",
|
|
"Package": "package",
|
|
"Action": "action",
|
|
"Commit": "commit",
|
|
"Git": "git",
|
|
"Contents": "content",
|
|
"File": "content",
|
|
"Wiki": "wiki",
|
|
"Comment": "comment",
|
|
"Review": "review",
|
|
"Reaction": "reaction",
|
|
"Topic": "topic",
|
|
"Status": "status",
|
|
"Combined": "status",
|
|
"Cron": "admin",
|
|
"Quota": "quota",
|
|
"OAuth2": "oauth",
|
|
"AccessToken": "oauth",
|
|
"API": "error",
|
|
"Forbidden": "error",
|
|
"NotFound": "error",
|
|
"NodeInfo": "federation",
|
|
"Activity": "activity",
|
|
"Feed": "activity",
|
|
"StopWatch": "time_tracking",
|
|
"TrackedTime": "time_tracking",
|
|
"Blocked": "user",
|
|
"Email": "user",
|
|
"Settings": "settings",
|
|
"GeneralAPI": "settings",
|
|
"GeneralAttachment": "settings",
|
|
"GeneralRepo": "settings",
|
|
"GeneralUI": "settings",
|
|
"Markdown": "misc",
|
|
"Markup": "misc",
|
|
"License": "misc",
|
|
"Gitignore": "misc",
|
|
"Annotated": "git",
|
|
"Note": "git",
|
|
"ChangedFile": "git",
|
|
"ExternalTracker": "repo",
|
|
"ExternalWiki": "repo",
|
|
"InternalTracker": "repo",
|
|
"Permission": "common",
|
|
"RepoTransfer": "repo",
|
|
"PayloadCommit": "hook",
|
|
"Dispatch": "action",
|
|
"Secret": "action",
|
|
"Variable": "action",
|
|
"Push": "repo",
|
|
"Mirror": "repo",
|
|
"Attachment": "common",
|
|
"EditDeadline": "issue",
|
|
"IssueDeadline": "issue",
|
|
"IssueLabels": "issue",
|
|
"IssueMeta": "issue",
|
|
"IssueTemplate": "issue",
|
|
"StateType": "common",
|
|
"TimeStamp": "common",
|
|
"Rename": "admin",
|
|
"Unadopted": "admin",
|
|
}
|
|
|
|
// classifyType determines which output file a type belongs to.
|
|
// It checks for a direct match first, then tries prefix matching,
|
|
// then strips Create/Edit/Delete/Update prefixes and Option suffix
|
|
// to find the base type's grouping.
|
|
func classifyType(name string) string {
|
|
// Direct match.
|
|
if group, ok := typeGrouping[name]; ok {
|
|
return group
|
|
}
|
|
|
|
// Prefix match — longest prefix wins.
|
|
bestKey := ""
|
|
bestGroup := ""
|
|
for key, group := range typeGrouping {
|
|
if core.HasPrefix(name, key) && len(key) > len(bestKey) {
|
|
bestKey = key
|
|
bestGroup = group
|
|
}
|
|
}
|
|
if bestGroup != "" {
|
|
return bestGroup
|
|
}
|
|
|
|
// Strip CRUD prefixes and Option suffix, then retry.
|
|
base := name
|
|
for _, prefix := range []string{"Create", "Edit", "Delete", "Update", "Add", "Submit", "Replace", "Set", "Transfer"} {
|
|
base = core.TrimPrefix(base, prefix)
|
|
}
|
|
base = core.TrimSuffix(base, "Option")
|
|
base = core.TrimSuffix(base, "Options")
|
|
|
|
if base != name && base != "" {
|
|
if group, ok := typeGrouping[base]; ok {
|
|
return group
|
|
}
|
|
// Prefix match on the stripped base.
|
|
bestKey = ""
|
|
bestGroup = ""
|
|
for key, group := range typeGrouping {
|
|
if core.HasPrefix(base, key) && len(key) > len(bestKey) {
|
|
bestKey = key
|
|
bestGroup = group
|
|
}
|
|
}
|
|
if bestGroup != "" {
|
|
return bestGroup
|
|
}
|
|
}
|
|
|
|
return "misc"
|
|
}
|
|
|
|
// sanitiseLine collapses a multi-line string into a single line,
|
|
// replacing newlines and consecutive whitespace with a single space.
|
|
func sanitiseLine(s string) string {
|
|
return core.Join(" ", splitFields(s)...)
|
|
}
|
|
|
|
// enumConstName generates a Go constant name for an enum value.
|
|
func enumConstName(typeName, value string) string {
|
|
return typeName + pascalCase(value)
|
|
}
|
|
|
|
// templateFuncs provides helper functions for the file template.
|
|
var templateFuncs = template.FuncMap{
|
|
"sanitise": sanitiseLine,
|
|
"enumConstName": enumConstName,
|
|
}
|
|
|
|
// fileHeader is the template for generating a single Go source file.
|
|
var fileHeader = template.Must(template.New("file").Funcs(templateFuncs).Parse(`// Code generated by forgegen from swagger.v1.json — DO NOT EDIT.
|
|
|
|
package types
|
|
{{if .NeedTime}}
|
|
import "time"
|
|
{{end}}
|
|
{{range .Types}}{{$t := .}}
|
|
{{- if .Description}}
|
|
// {{.Name}} — {{sanitise .Description}}
|
|
{{- end}}
|
|
{{- if .Usage}}
|
|
//
|
|
// Usage:
|
|
//
|
|
// opts := {{.Usage}}
|
|
{{- end}}
|
|
{{- if .IsEnum}}
|
|
type {{.Name}} string
|
|
|
|
const (
|
|
{{- range .EnumValues}}
|
|
{{enumConstName $t.Name .}} {{$t.Name}} = "{{.}}"
|
|
{{- end}}
|
|
)
|
|
{{- else if .IsAlias}}
|
|
type {{.Name}} {{.AliasType}}
|
|
{{- else if (eq (len .Fields) 0)}}
|
|
// {{.Name}} has no fields in the swagger spec.
|
|
type {{.Name}} struct{}
|
|
{{- else}}
|
|
type {{.Name}} struct {
|
|
{{- range .Fields}}
|
|
{{.GoName}} {{.GoType}} ` + "`" + `json:"{{.JSONName}}{{if not .Required}},omitempty{{end}}"` + "`" + `{{if .Comment}} // {{sanitise .Comment}}{{end}}
|
|
{{- end}}
|
|
}
|
|
{{- end}}
|
|
{{end}}
|
|
`))
|
|
|
|
// templateData is the data passed to the file template.
|
|
type templateData struct {
|
|
NeedTime bool
|
|
Types []*GoType
|
|
}
|
|
|
|
// Generate writes Go source files for the extracted types, grouped by logical domain.
|
|
//
|
|
// Usage:
|
|
//
|
|
// err := Generate(types, pairs, "types")
|
|
// _ = err
|
|
func Generate(types map[string]*GoType, pairs []CRUDPair, outDir string) error {
|
|
if err := coreio.Local.EnsureDir(outDir); err != nil {
|
|
return core.E("Generate", "create output directory", err)
|
|
}
|
|
|
|
populateUsageExamples(types)
|
|
|
|
// Group types by output file.
|
|
groups := make(map[string][]*GoType)
|
|
for _, gt := range types {
|
|
file := classifyType(gt.Name)
|
|
groups[file] = append(groups[file], gt)
|
|
}
|
|
|
|
// Sort types within each group for deterministic output.
|
|
for file := range groups {
|
|
slices.SortFunc(groups[file], func(a, b *GoType) int {
|
|
return cmp.Compare(a.Name, b.Name)
|
|
})
|
|
}
|
|
|
|
// Write each group to its own file.
|
|
fileNames := slices.Collect(maps.Keys(groups))
|
|
slices.Sort(fileNames)
|
|
|
|
for _, file := range fileNames {
|
|
outPath := core.JoinPath(outDir, file+".go")
|
|
if err := writeFile(outPath, groups[file]); err != nil {
|
|
return core.E("Generate", "write "+outPath, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func populateUsageExamples(types map[string]*GoType) {
|
|
for _, gt := range types {
|
|
if !shouldHaveUsage(gt.Name) {
|
|
continue
|
|
}
|
|
gt.Usage = usageExample(gt)
|
|
}
|
|
}
|
|
|
|
func shouldHaveUsage(name string) bool {
|
|
if core.HasSuffix(name, "Option") || core.HasSuffix(name, "Options") {
|
|
return true
|
|
}
|
|
for _, prefix := range []string{
|
|
"Create", "Edit", "Update", "Delete", "Add", "Set",
|
|
"Dispatch", "Migrate", "Generate", "Replace", "Submit", "Transfer",
|
|
} {
|
|
if core.HasPrefix(name, prefix) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func usageExample(gt *GoType) string {
|
|
example := exampleTypeLiteral(gt)
|
|
if example == "" {
|
|
example = gt.Name + "{}"
|
|
}
|
|
return example
|
|
}
|
|
|
|
func exampleTypeLiteral(gt *GoType) string {
|
|
if len(gt.Fields) == 0 {
|
|
return gt.Name + "{}"
|
|
}
|
|
|
|
field := chooseUsageField(gt.Fields)
|
|
if field.GoName == "" {
|
|
return gt.Name + "{}"
|
|
}
|
|
|
|
return gt.Name + "{" + field.GoName + ": " + exampleValue(field) + "}"
|
|
}
|
|
|
|
func chooseUsageField(fields []GoField) GoField {
|
|
best := fields[0]
|
|
bestScore := usageFieldScore(best)
|
|
for _, field := range fields[1:] {
|
|
score := usageFieldScore(field)
|
|
if score < bestScore || (score == bestScore && field.GoName < best.GoName) {
|
|
best = field
|
|
bestScore = score
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
|
|
func usageFieldScore(field GoField) int {
|
|
score := 100
|
|
if field.Required {
|
|
score -= 50
|
|
}
|
|
switch {
|
|
case core.HasSuffix(field.GoType, "string"):
|
|
score -= 30
|
|
case core.Contains(field.GoType, "time.Time"):
|
|
score -= 25
|
|
case core.HasSuffix(field.GoType, "bool"):
|
|
score -= 20
|
|
case core.Contains(field.GoType, "int"):
|
|
score -= 15
|
|
case core.HasPrefix(field.GoType, "[]"):
|
|
score -= 10
|
|
}
|
|
if core.Contains(field.GoName, "Name") || core.Contains(field.GoName, "Title") || core.Contains(field.GoName, "Body") || core.Contains(field.GoName, "Description") {
|
|
score -= 10
|
|
}
|
|
return score
|
|
}
|
|
|
|
func exampleValue(field GoField) string {
|
|
switch {
|
|
case core.HasPrefix(field.GoType, "*"):
|
|
return "&" + core.TrimPrefix(field.GoType, "*") + "{}"
|
|
case field.GoType == "string":
|
|
return exampleStringValue(field.GoName)
|
|
case field.GoType == "time.Time":
|
|
return "time.Now()"
|
|
case field.GoType == "bool":
|
|
return "true"
|
|
case core.HasSuffix(field.GoType, "int64"), core.HasSuffix(field.GoType, "int"), core.HasSuffix(field.GoType, "uint64"), core.HasSuffix(field.GoType, "uint"):
|
|
return "1"
|
|
case core.HasPrefix(field.GoType, "[]string"):
|
|
return "[]string{\"example\"}"
|
|
case core.HasPrefix(field.GoType, "[]int64"):
|
|
return "[]int64{1}"
|
|
case core.HasPrefix(field.GoType, "[]int"):
|
|
return "[]int{1}"
|
|
case core.HasPrefix(field.GoType, "map["):
|
|
return "map[string]string{\"key\": \"value\"}"
|
|
default:
|
|
return "{}"
|
|
}
|
|
}
|
|
|
|
func exampleStringValue(fieldName string) string {
|
|
switch {
|
|
case core.Contains(fieldName, "URL"):
|
|
return "\"https://example.com\""
|
|
case core.Contains(fieldName, "Email"):
|
|
return "\"alice@example.com\""
|
|
case core.Contains(fieldName, "Tag"):
|
|
return "\"v1.0.0\""
|
|
case core.Contains(fieldName, "Branch"), core.Contains(fieldName, "Ref"):
|
|
return "\"main\""
|
|
default:
|
|
return "\"example\""
|
|
}
|
|
}
|
|
|
|
// writeFile renders and writes a single Go source file for the given types.
|
|
func writeFile(path string, types []*GoType) error {
|
|
needTime := slices.ContainsFunc(types, func(gt *GoType) bool {
|
|
if core.Contains(gt.AliasType, "time.Time") {
|
|
return true
|
|
}
|
|
return slices.ContainsFunc(gt.Fields, func(f GoField) bool {
|
|
return core.Contains(f.GoType, "time.Time")
|
|
})
|
|
})
|
|
|
|
data := templateData{
|
|
NeedTime: needTime,
|
|
Types: types,
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := fileHeader.Execute(&buf, data); err != nil {
|
|
return core.E("writeFile", "execute template", err)
|
|
}
|
|
|
|
content := strings.TrimRight(buf.String(), "\n") + "\n"
|
|
if err := coreio.Local.Write(path, content); err != nil {
|
|
return core.E("writeFile", "write file", err)
|
|
}
|
|
|
|
return nil
|
|
}
|