2026-02-21 15:32:23 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
2026-03-26 18:00:20 +00:00
|
|
|
"cmp"
|
|
|
|
|
json "github.com/goccy/go-json"
|
feat: modernise to Go 1.26 iterators and stdlib helpers
Add ListIter in pagination + generic Resource.Iter for streaming
paginated results as iter.Seq2[T, error]. Add Iter* methods across
all service files (actions, admin, branches, issues, labels, notifs,
orgs, packages, pulls, releases, repos, teams, users, webhooks).
Modernise cmd/forgegen with slices.Sort, maps.Keys, strings.FieldsFuncSeq.
Co-Authored-By: Gemini <noreply@google.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-23 05:39:07 +00:00
|
|
|
"slices"
|
2026-03-16 19:06:09 +00:00
|
|
|
|
2026-03-26 13:27:06 +00:00
|
|
|
core "dappco.re/go/core"
|
2026-03-22 01:51:29 +00:00
|
|
|
coreio "dappco.re/go/core/io"
|
2026-02-21 15:32:23 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Spec represents a Swagger 2.0 specification document.
|
2026-03-26 18:00:20 +00:00
|
|
|
//
|
|
|
|
|
// Usage:
|
|
|
|
|
//
|
|
|
|
|
// spec, err := LoadSpec("testdata/swagger.v1.json")
|
|
|
|
|
// _ = spec
|
2026-02-21 15:32:23 +00:00
|
|
|
type Spec struct {
|
|
|
|
|
Swagger string `json:"swagger"`
|
|
|
|
|
Info SpecInfo `json:"info"`
|
|
|
|
|
Definitions map[string]SchemaDefinition `json:"definitions"`
|
|
|
|
|
Paths map[string]map[string]any `json:"paths"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SpecInfo holds metadata about the API specification.
|
2026-03-26 18:00:20 +00:00
|
|
|
//
|
|
|
|
|
// Usage:
|
|
|
|
|
//
|
|
|
|
|
// _ = SpecInfo{Title: "Forgejo API", Version: "1.0"}
|
2026-02-21 15:32:23 +00:00
|
|
|
type SpecInfo struct {
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
Version string `json:"version"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SchemaDefinition represents a single type definition in the swagger spec.
|
2026-03-26 18:00:20 +00:00
|
|
|
//
|
|
|
|
|
// Usage:
|
|
|
|
|
//
|
|
|
|
|
// _ = SchemaDefinition{Type: "object"}
|
2026-02-21 15:32:23 +00:00
|
|
|
type SchemaDefinition struct {
|
2026-04-02 01:50:01 +00:00
|
|
|
Description string `json:"description"`
|
2026-04-02 08:18:28 +00:00
|
|
|
Format string `json:"format"`
|
|
|
|
|
Ref string `json:"$ref"`
|
|
|
|
|
Items *SchemaProperty `json:"items"`
|
2026-04-02 01:50:01 +00:00
|
|
|
Type string `json:"type"`
|
|
|
|
|
Properties map[string]SchemaProperty `json:"properties"`
|
|
|
|
|
Required []string `json:"required"`
|
|
|
|
|
Enum []any `json:"enum"`
|
|
|
|
|
AdditionalProperties *SchemaProperty `json:"additionalProperties"`
|
|
|
|
|
XGoName string `json:"x-go-name"`
|
2026-02-21 15:32:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SchemaProperty represents a single property within a schema definition.
|
2026-03-26 18:00:20 +00:00
|
|
|
//
|
|
|
|
|
// Usage:
|
|
|
|
|
//
|
|
|
|
|
// _ = SchemaProperty{Type: "string"}
|
2026-02-21 15:32:23 +00:00
|
|
|
type SchemaProperty struct {
|
2026-04-02 01:50:01 +00:00
|
|
|
Type string `json:"type"`
|
|
|
|
|
Format string `json:"format"`
|
|
|
|
|
Description string `json:"description"`
|
|
|
|
|
Ref string `json:"$ref"`
|
|
|
|
|
Items *SchemaProperty `json:"items"`
|
|
|
|
|
Enum []any `json:"enum"`
|
|
|
|
|
AdditionalProperties *SchemaProperty `json:"additionalProperties"`
|
|
|
|
|
XGoName string `json:"x-go-name"`
|
2026-02-21 15:32:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GoType is the intermediate representation for a Go type to be generated.
|
2026-03-26 18:00:20 +00:00
|
|
|
//
|
|
|
|
|
// Usage:
|
|
|
|
|
//
|
|
|
|
|
// _ = GoType{Name: "Repository"}
|
2026-02-21 15:32:23 +00:00
|
|
|
type GoType struct {
|
|
|
|
|
Name string
|
|
|
|
|
Description string
|
2026-04-02 07:14:47 +00:00
|
|
|
Usage string
|
2026-02-21 15:32:23 +00:00
|
|
|
Fields []GoField
|
|
|
|
|
IsEnum bool
|
|
|
|
|
EnumValues []string
|
2026-04-02 01:50:01 +00:00
|
|
|
IsAlias bool
|
|
|
|
|
AliasType string
|
2026-02-21 15:32:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GoField is the intermediate representation for a single struct field.
|
2026-03-26 18:00:20 +00:00
|
|
|
//
|
|
|
|
|
// Usage:
|
|
|
|
|
//
|
|
|
|
|
// _ = GoField{GoName: "ID", GoType: "int64"}
|
2026-02-21 15:32:23 +00:00
|
|
|
type GoField struct {
|
|
|
|
|
GoName string
|
|
|
|
|
GoType string
|
|
|
|
|
JSONName string
|
|
|
|
|
Comment string
|
|
|
|
|
Required bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CRUDPair groups a base type with its corresponding Create and Edit option types.
|
2026-03-26 18:00:20 +00:00
|
|
|
//
|
|
|
|
|
// Usage:
|
|
|
|
|
//
|
|
|
|
|
// _ = CRUDPair{Base: "Repository", Create: "CreateRepoOption", Edit: "EditRepoOption"}
|
2026-02-21 15:32:23 +00:00
|
|
|
type CRUDPair struct {
|
|
|
|
|
Base string
|
|
|
|
|
Create string
|
|
|
|
|
Edit string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LoadSpec reads and parses a Swagger 2.0 JSON file from the given path.
|
2026-03-26 18:00:20 +00:00
|
|
|
//
|
|
|
|
|
// Usage:
|
|
|
|
|
//
|
|
|
|
|
// spec, err := LoadSpec("testdata/swagger.v1.json")
|
|
|
|
|
// _ = spec
|
2026-02-21 15:32:23 +00:00
|
|
|
func LoadSpec(path string) (*Spec, error) {
|
2026-03-16 19:06:09 +00:00
|
|
|
content, err := coreio.Local.Read(path)
|
2026-02-21 15:32:23 +00:00
|
|
|
if err != nil {
|
2026-03-26 18:00:20 +00:00
|
|
|
return nil, core.E("LoadSpec", "read spec", err)
|
2026-02-21 15:32:23 +00:00
|
|
|
}
|
|
|
|
|
var spec Spec
|
2026-03-26 18:00:20 +00:00
|
|
|
if err := json.Unmarshal([]byte(content), &spec); err != nil {
|
|
|
|
|
return nil, core.E("LoadSpec", "parse spec", err)
|
2026-02-21 15:32:23 +00:00
|
|
|
}
|
|
|
|
|
return &spec, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ExtractTypes converts all swagger definitions into Go type intermediate representations.
|
2026-03-26 18:00:20 +00:00
|
|
|
//
|
|
|
|
|
// Usage:
|
|
|
|
|
//
|
|
|
|
|
// types := ExtractTypes(spec)
|
|
|
|
|
// _ = types["Repository"]
|
2026-02-21 15:32:23 +00:00
|
|
|
func ExtractTypes(spec *Spec) map[string]*GoType {
|
|
|
|
|
result := make(map[string]*GoType)
|
|
|
|
|
for name, def := range spec.Definitions {
|
|
|
|
|
gt := &GoType{Name: name, Description: def.Description}
|
|
|
|
|
if len(def.Enum) > 0 {
|
|
|
|
|
gt.IsEnum = true
|
|
|
|
|
for _, v := range def.Enum {
|
2026-03-26 18:00:20 +00:00
|
|
|
gt.EnumValues = append(gt.EnumValues, core.Sprint(v))
|
2026-02-21 15:32:23 +00:00
|
|
|
}
|
feat: modernise to Go 1.26 iterators and stdlib helpers
Add ListIter in pagination + generic Resource.Iter for streaming
paginated results as iter.Seq2[T, error]. Add Iter* methods across
all service files (actions, admin, branches, issues, labels, notifs,
orgs, packages, pulls, releases, repos, teams, users, webhooks).
Modernise cmd/forgegen with slices.Sort, maps.Keys, strings.FieldsFuncSeq.
Co-Authored-By: Gemini <noreply@google.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-23 05:39:07 +00:00
|
|
|
slices.Sort(gt.EnumValues)
|
2026-02-21 15:32:23 +00:00
|
|
|
result[name] = gt
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-04-02 08:18:28 +00:00
|
|
|
|
|
|
|
|
if aliasType, ok := definitionAliasType(def, spec.Definitions); ok {
|
2026-04-02 01:50:01 +00:00
|
|
|
gt.IsAlias = true
|
2026-04-02 08:18:28 +00:00
|
|
|
gt.AliasType = aliasType
|
2026-04-02 01:50:01 +00:00
|
|
|
result[name] = gt
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-02-21 15:32:23 +00:00
|
|
|
required := make(map[string]bool)
|
|
|
|
|
for _, r := range def.Required {
|
|
|
|
|
required[r] = true
|
|
|
|
|
}
|
|
|
|
|
for fieldName, prop := range def.Properties {
|
|
|
|
|
goName := prop.XGoName
|
|
|
|
|
if goName == "" {
|
|
|
|
|
goName = pascalCase(fieldName)
|
|
|
|
|
}
|
|
|
|
|
gf := GoField{
|
|
|
|
|
GoName: goName,
|
2026-04-02 08:18:28 +00:00
|
|
|
GoType: resolveGoType(prop, spec.Definitions),
|
2026-02-21 15:32:23 +00:00
|
|
|
JSONName: fieldName,
|
|
|
|
|
Comment: prop.Description,
|
|
|
|
|
Required: required[fieldName],
|
|
|
|
|
}
|
|
|
|
|
gt.Fields = append(gt.Fields, gf)
|
|
|
|
|
}
|
feat: modernise to Go 1.26 iterators and stdlib helpers
Add ListIter in pagination + generic Resource.Iter for streaming
paginated results as iter.Seq2[T, error]. Add Iter* methods across
all service files (actions, admin, branches, issues, labels, notifs,
orgs, packages, pulls, releases, repos, teams, users, webhooks).
Modernise cmd/forgegen with slices.Sort, maps.Keys, strings.FieldsFuncSeq.
Co-Authored-By: Gemini <noreply@google.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-23 05:39:07 +00:00
|
|
|
slices.SortFunc(gt.Fields, func(a, b GoField) int {
|
2026-03-26 18:00:20 +00:00
|
|
|
return cmp.Compare(a.GoName, b.GoName)
|
2026-02-21 15:32:23 +00:00
|
|
|
})
|
|
|
|
|
result[name] = gt
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 08:18:28 +00:00
|
|
|
func definitionAliasType(def SchemaDefinition, defs map[string]SchemaDefinition) (string, bool) {
|
|
|
|
|
if def.Ref != "" {
|
|
|
|
|
return refName(def.Ref), true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch def.Type {
|
|
|
|
|
case "string":
|
|
|
|
|
return "string", true
|
|
|
|
|
case "integer":
|
|
|
|
|
switch def.Format {
|
|
|
|
|
case "int64":
|
|
|
|
|
return "int64", true
|
|
|
|
|
case "int32":
|
|
|
|
|
return "int32", true
|
|
|
|
|
default:
|
|
|
|
|
return "int", true
|
|
|
|
|
}
|
|
|
|
|
case "number":
|
|
|
|
|
switch def.Format {
|
|
|
|
|
case "float":
|
|
|
|
|
return "float32", true
|
|
|
|
|
default:
|
|
|
|
|
return "float64", true
|
|
|
|
|
}
|
|
|
|
|
case "boolean":
|
|
|
|
|
return "bool", true
|
|
|
|
|
case "array":
|
|
|
|
|
if def.Items != nil {
|
|
|
|
|
return "[]" + resolveGoType(*def.Items, defs), true
|
|
|
|
|
}
|
|
|
|
|
return "[]any", true
|
|
|
|
|
case "object":
|
|
|
|
|
if def.AdditionalProperties != nil {
|
|
|
|
|
return resolveMapType(*def.AdditionalProperties, defs), true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 15:32:23 +00:00
|
|
|
// DetectCRUDPairs finds Create*Option / Edit*Option pairs in the swagger definitions
|
|
|
|
|
// and maps them back to the base type name.
|
2026-03-26 18:00:20 +00:00
|
|
|
//
|
|
|
|
|
// Usage:
|
|
|
|
|
//
|
|
|
|
|
// pairs := DetectCRUDPairs(spec)
|
|
|
|
|
// _ = pairs
|
2026-02-21 15:32:23 +00:00
|
|
|
func DetectCRUDPairs(spec *Spec) []CRUDPair {
|
|
|
|
|
var pairs []CRUDPair
|
|
|
|
|
for name := range spec.Definitions {
|
2026-03-26 13:27:06 +00:00
|
|
|
if !core.HasPrefix(name, "Create") || !core.HasSuffix(name, "Option") {
|
2026-02-21 15:32:23 +00:00
|
|
|
continue
|
|
|
|
|
}
|
2026-03-26 13:27:06 +00:00
|
|
|
inner := core.TrimPrefix(name, "Create")
|
|
|
|
|
inner = core.TrimSuffix(inner, "Option")
|
2026-03-26 18:00:20 +00:00
|
|
|
editName := core.Concat("Edit", inner, "Option")
|
2026-02-21 15:32:23 +00:00
|
|
|
pair := CRUDPair{Base: inner, Create: name}
|
|
|
|
|
if _, ok := spec.Definitions[editName]; ok {
|
|
|
|
|
pair.Edit = editName
|
|
|
|
|
}
|
|
|
|
|
pairs = append(pairs, pair)
|
|
|
|
|
}
|
feat: modernise to Go 1.26 iterators and stdlib helpers
Add ListIter in pagination + generic Resource.Iter for streaming
paginated results as iter.Seq2[T, error]. Add Iter* methods across
all service files (actions, admin, branches, issues, labels, notifs,
orgs, packages, pulls, releases, repos, teams, users, webhooks).
Modernise cmd/forgegen with slices.Sort, maps.Keys, strings.FieldsFuncSeq.
Co-Authored-By: Gemini <noreply@google.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-23 05:39:07 +00:00
|
|
|
slices.SortFunc(pairs, func(a, b CRUDPair) int {
|
2026-03-26 18:00:20 +00:00
|
|
|
return cmp.Compare(a.Base, b.Base)
|
2026-02-21 15:32:23 +00:00
|
|
|
})
|
|
|
|
|
return pairs
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// resolveGoType maps a swagger schema property to a Go type string.
|
2026-04-02 08:18:28 +00:00
|
|
|
func resolveGoType(prop SchemaProperty, defs map[string]SchemaDefinition) string {
|
2026-02-21 15:32:23 +00:00
|
|
|
if prop.Ref != "" {
|
2026-04-02 08:18:28 +00:00
|
|
|
return refGoType(prop.Ref, defs)
|
2026-02-21 15:32:23 +00:00
|
|
|
}
|
|
|
|
|
switch prop.Type {
|
|
|
|
|
case "string":
|
|
|
|
|
switch prop.Format {
|
|
|
|
|
case "date-time":
|
|
|
|
|
return "time.Time"
|
|
|
|
|
case "binary":
|
|
|
|
|
return "[]byte"
|
|
|
|
|
default:
|
|
|
|
|
return "string"
|
|
|
|
|
}
|
|
|
|
|
case "integer":
|
|
|
|
|
switch prop.Format {
|
|
|
|
|
case "int64":
|
|
|
|
|
return "int64"
|
|
|
|
|
case "int32":
|
|
|
|
|
return "int32"
|
|
|
|
|
default:
|
|
|
|
|
return "int"
|
|
|
|
|
}
|
|
|
|
|
case "number":
|
|
|
|
|
switch prop.Format {
|
|
|
|
|
case "float":
|
|
|
|
|
return "float32"
|
|
|
|
|
default:
|
|
|
|
|
return "float64"
|
|
|
|
|
}
|
|
|
|
|
case "boolean":
|
|
|
|
|
return "bool"
|
|
|
|
|
case "array":
|
|
|
|
|
if prop.Items != nil {
|
2026-04-02 08:18:28 +00:00
|
|
|
return "[]" + resolveGoType(*prop.Items, defs)
|
2026-02-21 15:32:23 +00:00
|
|
|
}
|
|
|
|
|
return "[]any"
|
|
|
|
|
case "object":
|
2026-04-02 08:18:28 +00:00
|
|
|
return resolveMapType(prop, defs)
|
2026-02-21 15:32:23 +00:00
|
|
|
default:
|
|
|
|
|
return "any"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 01:50:01 +00:00
|
|
|
// resolveMapType maps a swagger object with additionalProperties to a Go map type.
|
2026-04-02 08:18:28 +00:00
|
|
|
func resolveMapType(prop SchemaProperty, defs map[string]SchemaDefinition) string {
|
2026-04-02 01:50:01 +00:00
|
|
|
valueType := "any"
|
|
|
|
|
if prop.AdditionalProperties != nil {
|
2026-04-02 08:18:28 +00:00
|
|
|
valueType = resolveGoType(*prop.AdditionalProperties, defs)
|
2026-04-02 01:50:01 +00:00
|
|
|
}
|
|
|
|
|
return "map[string]" + valueType
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 08:18:28 +00:00
|
|
|
func refName(ref string) string {
|
|
|
|
|
parts := core.Split(ref, "/")
|
|
|
|
|
return parts[len(parts)-1]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func refGoType(ref string, defs map[string]SchemaDefinition) string {
|
|
|
|
|
name := refName(ref)
|
|
|
|
|
def, ok := defs[name]
|
|
|
|
|
if !ok {
|
|
|
|
|
return "*" + name
|
|
|
|
|
}
|
|
|
|
|
if definitionNeedsPointer(def) {
|
|
|
|
|
return "*" + name
|
|
|
|
|
}
|
|
|
|
|
return name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func definitionNeedsPointer(def SchemaDefinition) bool {
|
|
|
|
|
if len(def.Enum) > 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if def.Ref != "" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
switch def.Type {
|
|
|
|
|
case "string", "integer", "number", "boolean", "array":
|
|
|
|
|
return false
|
|
|
|
|
case "object":
|
|
|
|
|
return true
|
|
|
|
|
default:
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 15:32:23 +00:00
|
|
|
// pascalCase converts a snake_case or kebab-case string to PascalCase,
|
|
|
|
|
// with common acronyms kept uppercase.
|
|
|
|
|
func pascalCase(s string) string {
|
feat: modernise to Go 1.26 iterators and stdlib helpers
Add ListIter in pagination + generic Resource.Iter for streaming
paginated results as iter.Seq2[T, error]. Add Iter* methods across
all service files (actions, admin, branches, issues, labels, notifs,
orgs, packages, pulls, releases, repos, teams, users, webhooks).
Modernise cmd/forgegen with slices.Sort, maps.Keys, strings.FieldsFuncSeq.
Co-Authored-By: Gemini <noreply@google.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-23 05:39:07 +00:00
|
|
|
var parts []string
|
2026-03-26 18:00:20 +00:00
|
|
|
for _, p := range splitSnakeKebab(s) {
|
2026-02-21 15:32:23 +00:00
|
|
|
if len(p) == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-03-26 13:27:06 +00:00
|
|
|
upper := core.Upper(p)
|
2026-02-21 15:32:23 +00:00
|
|
|
switch upper {
|
|
|
|
|
case "ID", "URL", "HTML", "SSH", "HTTP", "HTTPS", "API", "URI", "GPG", "IP", "CSS", "JS":
|
feat: modernise to Go 1.26 iterators and stdlib helpers
Add ListIter in pagination + generic Resource.Iter for streaming
paginated results as iter.Seq2[T, error]. Add Iter* methods across
all service files (actions, admin, branches, issues, labels, notifs,
orgs, packages, pulls, releases, repos, teams, users, webhooks).
Modernise cmd/forgegen with slices.Sort, maps.Keys, strings.FieldsFuncSeq.
Co-Authored-By: Gemini <noreply@google.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-23 05:39:07 +00:00
|
|
|
parts = append(parts, upper)
|
2026-02-21 15:32:23 +00:00
|
|
|
default:
|
2026-03-26 18:00:20 +00:00
|
|
|
parts = append(parts, core.Concat(core.Upper(p[:1]), p[1:]))
|
2026-02-21 15:32:23 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-26 18:00:20 +00:00
|
|
|
return core.Concat(parts...)
|
2026-02-21 15:32:23 +00:00
|
|
|
}
|