mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-13 06:40:59 +00:00
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).
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
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.`
|
||||
Reference in New Issue
Block a user