mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-28 16:09:36 +00:00
Add tools/openapigen — a single-binary Go program that walks the
exported structs in database/model, web/entity, and xray via go/parser
and emits two committed artifacts under frontend/src/generated:
- zod.ts shared Zod schemas keyed off `validate:` tags (ports get
.min(1).max(65535), Inbound.protocol becomes a z.enum,
Node.scheme too, etc.)
- types.ts plain TS interfaces inferred from the same walk, so
consumers can import Inbound without dragging Zod along
The walker flattens embedded structs (AllSettingView.AllSetting),
honors json:"-" and omitempty, and accepts per-struct overrides so
the JSON-string-inside-JSON columns (Inbound.Settings/StreamSettings/
Sniffing, ClientRecord.Reverse, InboundClientIps.Ips) render as
z.unknown() instead of leaking the DB-storage type into the API
contract. Type aliases like model.Protocol are emitted as TS aliases
and Zod schemas in their own right.
Wires `npm run gen:zod` in frontend/package.json so the generator can
be re-run without leaving the frontend tree. The existing openapi.json
build (gen:api) is left alone for now; migrating the OpenAPI surface
to this generator is a follow-up.
PR2 of the planned Zod end-to-end rollout.
173 lines
3.3 KiB
Go
173 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type Schema struct {
|
|
Name string
|
|
Package string
|
|
Fields []Field
|
|
Doc string
|
|
}
|
|
|
|
type Alias struct {
|
|
Name string
|
|
Package string
|
|
Underlying TypeRef
|
|
}
|
|
|
|
type Field struct {
|
|
JSONName string
|
|
GoName string
|
|
Type TypeRef
|
|
Optional bool
|
|
Skip bool
|
|
Validate []ValidateRule
|
|
Doc string
|
|
}
|
|
|
|
type TypeRef struct {
|
|
Kind TypeKind
|
|
Name string
|
|
Element *TypeRef
|
|
Key *TypeRef
|
|
Value *TypeRef
|
|
Inner *TypeRef
|
|
}
|
|
|
|
type TypeKind string
|
|
|
|
const (
|
|
KindString TypeKind = "string"
|
|
KindNumber TypeKind = "number"
|
|
KindInt TypeKind = "int"
|
|
KindBool TypeKind = "boolean"
|
|
KindArray TypeKind = "array"
|
|
KindMap TypeKind = "map"
|
|
KindObject TypeKind = "object"
|
|
KindRef TypeKind = "ref"
|
|
KindUnknown TypeKind = "unknown"
|
|
KindAny TypeKind = "any"
|
|
KindRaw TypeKind = "raw"
|
|
)
|
|
|
|
type ValidateRule struct {
|
|
Name string
|
|
Param string
|
|
}
|
|
|
|
func parseStructTag(raw string) (json string, validate string, gormHasDash bool) {
|
|
tag := reflect.StructTag(strings.Trim(raw, "`"))
|
|
json = tag.Get("json")
|
|
validate = tag.Get("validate")
|
|
if g := tag.Get("gorm"); g != "" {
|
|
for _, part := range strings.Split(g, ";") {
|
|
if strings.TrimSpace(part) == "-" {
|
|
gormHasDash = true
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func parseJSONTag(tag string) (name string, omit bool, omitempty bool) {
|
|
if tag == "" {
|
|
return "", false, false
|
|
}
|
|
parts := strings.Split(tag, ",")
|
|
name = parts[0]
|
|
if name == "-" {
|
|
return "", true, false
|
|
}
|
|
for _, p := range parts[1:] {
|
|
if p == "omitempty" {
|
|
omitempty = true
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func parseValidateTag(tag string) []ValidateRule {
|
|
if tag == "" {
|
|
return nil
|
|
}
|
|
var rules []ValidateRule
|
|
for _, part := range strings.Split(tag, ",") {
|
|
part = strings.TrimSpace(part)
|
|
if part == "" {
|
|
continue
|
|
}
|
|
eq := strings.IndexByte(part, '=')
|
|
if eq < 0 {
|
|
rules = append(rules, ValidateRule{Name: part})
|
|
continue
|
|
}
|
|
rules = append(rules, ValidateRule{Name: part[:eq], Param: part[eq+1:]})
|
|
}
|
|
return rules
|
|
}
|
|
|
|
func (s Schema) HasValidationOn(field string) bool {
|
|
for _, f := range s.Fields {
|
|
if f.JSONName == field {
|
|
return len(f.Validate) > 0
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func sortSchemas(in []Schema) []Schema {
|
|
out := make([]Schema, len(in))
|
|
copy(out, in)
|
|
sort.Slice(out, func(i, j int) bool {
|
|
return out[i].Name < out[j].Name
|
|
})
|
|
return out
|
|
}
|
|
|
|
func sortAliases(in []Alias) []Alias {
|
|
out := make([]Alias, len(in))
|
|
copy(out, in)
|
|
sort.Slice(out, func(i, j int) bool {
|
|
return out[i].Name < out[j].Name
|
|
})
|
|
return out
|
|
}
|
|
|
|
func flattenEmbedded(schemas []Schema) []Schema {
|
|
byName := make(map[string]Schema, len(schemas))
|
|
for _, s := range schemas {
|
|
byName[s.Name] = s
|
|
}
|
|
out := make([]Schema, 0, len(schemas))
|
|
for _, s := range schemas {
|
|
var resolved []Field
|
|
seen := make(map[string]bool, len(s.Fields))
|
|
for _, f := range s.Fields {
|
|
if f.Type.Kind == KindRef && f.Type.Name != "nullable" {
|
|
if embedded, ok := byName[f.Type.Name]; ok && f.GoName == f.Type.Name {
|
|
for _, ef := range embedded.Fields {
|
|
if seen[ef.JSONName] {
|
|
continue
|
|
}
|
|
seen[ef.JSONName] = true
|
|
resolved = append(resolved, ef)
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
if seen[f.JSONName] {
|
|
continue
|
|
}
|
|
seen[f.JSONName] = true
|
|
resolved = append(resolved, f)
|
|
}
|
|
s.Fields = resolved
|
|
out = append(out, s)
|
|
}
|
|
return out
|
|
}
|