Files
3x-ui/tools/openapigen/emit_examples.go
MHSanaei 83799d71b0 feat(api-docs): generate response examples from Go structs; fix SS2022 PSK regen (#4996)
Stop hand-writing OpenAPI response examples, which kept drifting from the real payloads (clients/traffic missing fields, inbounds/list exposing userId which is json:"-", the fictional inbound-443 tag instead of the real in-<port>-<transport> form).

tools/openapigen now emits frontend/src/generated/examples.ts: a per-struct example instance built from type defaults, validate oneof/min bounds, and example: struct tags, with nested-ref expansion and a cycle guard. build-openapi.mjs composes the {success,obj} envelope from it for any endpoint annotated with responseSchema (+ responseSchemaArray for lists); the hand-written response is dropped for those. Service DTOs InboundOption/ApiTokenView/ProbeResultUI are added to the walker.

#4996: client password regeneration now produces a valid Shadowsocks 2022 PSK (correct base64 length per cipher) when an SS2022 inbound is attached, in both the single and bulk client forms; backend surfaces ssMethod on /inbounds/options so the UI can pick the right length.

Also: Swagger UI persists the Authorization token across reloads (persistAuthorization).
2026-06-06 14:58:15 +02:00

172 lines
3.7 KiB
Go

package main
import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
)
func emitExamples(w io.Writer, schemas []Schema, aliases []Alias) error {
byName := make(map[string]Schema, len(schemas))
for _, s := range schemas {
byName[s.Name] = s
}
aliasByName := make(map[string]Alias, len(aliases))
for _, a := range aliases {
aliasByName[a.Name] = a
}
gen := &exampleGen{byName: byName, aliasByName: aliasByName}
out := make(map[string]any, len(schemas))
for _, s := range schemas {
out[s.Name] = gen.forSchema(s, map[string]bool{})
}
payload, err := json.MarshalIndent(out, "", " ")
if err != nil {
return err
}
if _, err := fmt.Fprintln(w, examplesHeader); err != nil {
return err
}
if _, err := fmt.Fprintf(w, "export const EXAMPLES: Record<string, unknown> = %s;\n", payload); err != nil {
return err
}
return nil
}
type exampleGen struct {
byName map[string]Schema
aliasByName map[string]Alias
}
func (g *exampleGen) forSchema(s Schema, visited map[string]bool) map[string]any {
obj := make(map[string]any, len(s.Fields))
for _, f := range s.Fields {
obj[f.JSONName] = g.forField(f, visited)
}
return obj
}
func (g *exampleGen) forField(f Field, visited map[string]bool) any {
if f.Example != "" {
return coerceExample(f.Example, baseKind(f.Type))
}
if v, ok := firstOneOf(f.Validate); ok {
return v
}
bk := baseKind(f.Type)
if bk.Kind == KindInt || bk.Kind == KindNumber {
if v, ok := numericFloor(bk.Kind, f.Validate); ok {
return v
}
}
return g.forType(f.Type, visited)
}
func (g *exampleGen) forType(t TypeRef, visited map[string]bool) any {
switch t.Kind {
case KindString:
if t.Name == "datetime" {
return "2025-01-01T00:00:00Z"
}
return ""
case KindInt, KindNumber:
return 0
case KindBool:
return false
case KindArray:
if isVisitedRef(*t.Element, visited) {
return []any{}
}
return []any{g.forType(*t.Element, visited)}
case KindMap:
return map[string]any{}
case KindRef:
if t.Name == "nullable" {
return nil
}
if alias, ok := g.aliasByName[t.Name]; ok {
return g.forType(alias.Underlying, visited)
}
schema, ok := g.byName[t.Name]
if !ok || visited[t.Name] {
return map[string]any{}
}
next := cloneVisited(visited)
next[t.Name] = true
return g.forSchema(schema, next)
}
return nil
}
func baseKind(t TypeRef) TypeRef {
if t.Kind == KindRef && t.Name == "nullable" && t.Inner != nil {
return *t.Inner
}
return t
}
func isVisitedRef(t TypeRef, visited map[string]bool) bool {
return t.Kind == KindRef && t.Name != "nullable" && visited[t.Name]
}
func cloneVisited(in map[string]bool) map[string]bool {
out := make(map[string]bool, len(in)+1)
for k, v := range in {
out[k] = v
}
return out
}
func numericFloor(kind TypeKind, rules []ValidateRule) (any, bool) {
for _, r := range rules {
if (r.Name == "gte" || r.Name == "min") && r.Param != "" {
return coerceExample(r.Param, TypeRef{Kind: kind}), true
}
}
return nil, false
}
func firstOneOf(rules []ValidateRule) (string, bool) {
for _, r := range rules {
if r.Name == "oneof" {
fields := strings.Fields(r.Param)
if len(fields) > 0 {
return fields[0], true
}
}
}
return "", false
}
func coerceExample(ex string, t TypeRef) any {
switch t.Kind {
case KindInt:
if n, err := strconv.ParseInt(ex, 10, 64); err == nil {
return n
}
return 0
case KindNumber:
if n, err := strconv.ParseFloat(ex, 64); err == nil {
return n
}
return 0
case KindBool:
return ex == "true"
case KindString:
return ex
default:
var parsed any
if err := json.Unmarshal([]byte(ex), &parsed); err == nil {
return parsed
}
return ex
}
}
const examplesHeader = `// Code generated by tools/openapigen. DO NOT EDIT.`