Files
3x-ui/tools/openapigen/main.go
T
Kuzz007 34c43c8a9d fix: address the automated review round on PR #6154
- Replace parseGeodataFile's full proto.Unmarshal with a protowire-based
  scan that reads only each entry's Code, skipping every Domain/CIDR
  payload without allocating it -- the actual bulk of a real
  geoip.dat/geosite.dat. Also caps the file read at 256 MiB.
- Hold geodataMu across the full scan-and-maybe-parse in
  GetGeodataCategories instead of releasing it around the parse, so
  concurrent cache misses (e.g. several browser tabs) can't all
  independently re-parse every file; clone the cached slices before
  returning them so a caller mutating its result can't corrupt the cache.
- Gate useGeodataCategories on the rule editor's own `open` state instead
  of firing on every visit to the Routing tab.
- formatGeodataSuggestion now compares filenames with strings.EqualFold,
  matching scanGeodataFiles' own case-insensitive match -- a file that IS
  the default one on a case-insensitive filesystem (e.g. Windows) no
  longer gets the long ext: form.
- Fix a real bug the review's hypothesis led to: Select mode="tags" only
  commits the search text on Enter/comma, so clicking Save right after
  typing (a blur, not an Enter) silently dropped the value entirely, with
  no domain/ip key at all in the saved rule. Wrap it in a small
  TagsAutocomplete that also commits on blur. Same autocomplete now
  applies to sourceIP, which accepts geoip:/ext: too.
- Guard useGeodataCategories' fetch per-field with Array.isArray instead
  of a single top-level `?? EMPTY_CATEGORIES`, since parseMsg returns the
  original unvalidated obj (not null) on a schema mismatch.
- Test fixes: exact slices.Equal instead of slices.Contains-only
  assertions, t.Run subtests, a cache-hit-skips-reparse test (via a
  test-only parse counter), a returns-independent-slices test, a
  file-size-cap test, and four new frontend tests covering the tags
  round-trip including the blur-commit regression above.
- GeodataCategories now goes through the same generated-example path as
  every other response type (StructAllow + example: tags + responseSchema
  in endpoints.ts) instead of a hand-written response string. The
  existing hand-written GeodataCategoriesSchema in schemas/routing.ts is
  unrelated to this and is left alone -- CLAUDE.md is explicit that Zod
  schemas under src/schemas/ are the source of truth and only the
  generated example/openapi path comes from Go example: tags.
- Drop the two PR-illustration screenshots from media/ -- nothing in the
  repo referenced them; they only ever needed to exist in the PR
  description itself.

Not changed: leaving geodataFileKind's leak into generated/{types,zod}.ts
as-is. internal/web/service's openapigen request has no AliasAllow at
all, so every non-struct type in the package already leaks this way
(e.g. staticEgressResolver, transportBits predate this PR) -- scoping an
AliasAllow for the whole package is a real cleanup but a separate, wider
change than this PR's own footprint, and needs checking nothing already
depends on those existing generated aliases first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 01:18:00 +03:00

154 lines
3.4 KiB
Go

package main
import (
"bytes"
"flag"
"fmt"
"os"
"path/filepath"
)
func main() {
root := flag.String("root", ".", "repository root containing internal/database/model and internal/web/entity")
outDir := flag.String("out", "frontend/src/generated", "output directory relative to root")
flag.Parse()
if err := run(*root, *outDir); err != nil {
fmt.Fprintln(os.Stderr, "openapigen:", err)
os.Exit(1)
}
}
func run(root, outDir string) error {
requests := []packageRequest{
{
Path: resolveRel(root, "internal/database/model"),
StructAllow: setOf(
"User",
"Inbound",
"FallbackParentInfo",
"OutboundTraffics",
"InboundClientIps",
"ApiToken",
"HistoryOfSeeders",
"Setting",
"Node",
"ClientReverse",
"Client",
"ClientRecord",
"ClientInbound",
"InboundFallback",
"Host",
),
AliasAllow: setOf("Protocol"),
Overrides: map[string][]walkOverride{
"Inbound": {
{Field: "Settings", Kind: KindAny},
{Field: "StreamSettings", Kind: KindAny},
{Field: "Sniffing", Kind: KindAny},
},
"ClientRecord": {
{Field: "Reverse", Kind: KindAny},
},
"InboundClientIps": {
{Field: "Ips", Kind: KindAny},
},
"Host": {
{Field: "MuxParams", Kind: KindAny},
{Field: "SockoptParams", Kind: KindAny},
},
},
},
{
Path: resolveRel(root, "internal/web/entity"),
StructAllow: setOf(
"Msg",
"AllSetting",
"AllSettingView",
"HostGroup",
),
},
{
Path: resolveRel(root, "internal/xray"),
StructAllow: setOf(
"ClientTraffic",
),
},
{
Path: resolveRel(root, "internal/web/service"),
StructAllow: setOf(
"InboundOption",
"NodeMutationRequest",
"NodeView",
"ProbeResultUI",
"RealityScanResult",
"GeodataCategories",
),
},
{
Path: resolveRel(root, "internal/web/service/panel"),
StructAllow: setOf("ApiTokenView", "PanelUpdateStatus"),
},
{
Path: resolveRel(root, "internal/amneziawg"),
StructAllow: setOf("ServerSettings"),
},
}
schemas, aliases, err := walkPackages(requests)
if err != nil {
return err
}
schemas = flattenEmbedded(schemas)
if len(schemas) == 0 {
return fmt.Errorf("no schemas produced; nothing to write")
}
target := filepath.Join(root, outDir)
if err := os.MkdirAll(target, 0o755); err != nil {
return err
}
zodBuf := &bytes.Buffer{}
if err := emitZod(zodBuf, schemas, aliases); err != nil {
return err
}
typesBuf := &bytes.Buffer{}
if err := emitTypes(typesBuf, schemas, aliases); err != nil {
return err
}
examplesBuf := &bytes.Buffer{}
if err := emitExamples(examplesBuf, schemas, aliases); err != nil {
return err
}
schemasBuf := &bytes.Buffer{}
if err := emitJSONSchema(schemasBuf, schemas, aliases); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(target, "zod.ts"), zodBuf.Bytes(), 0o644); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(target, "types.ts"), typesBuf.Bytes(), 0o644); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(target, "examples.ts"), examplesBuf.Bytes(), 0o644); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(target, "schemas.ts"), schemasBuf.Bytes(), 0o644); err != nil {
return err
}
fmt.Printf("openapigen: wrote %d schemas to %s\n", len(schemas), target)
return nil
}
func setOf(names ...string) map[string]bool {
m := make(map[string]bool, len(names))
for _, n := range names {
m[n] = true
}
return m
}