mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-08 13:24:33 +00:00
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).
172 lines
3.7 KiB
Go
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.`
|