feat: add backend, database, and protocol foundations
This commit is contained in:
commit
ec08d487e1
179 files changed
+27079
No files matched your search
@@ -0,0 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/dihemoe/alisten/internal/bootstrap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(bootstrap.Main(os.Args[1:]))
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type goPattern struct {
|
||||
name, expr string
|
||||
}
|
||||
|
||||
type goDecoder struct {
|
||||
b bytes.Buffer
|
||||
patterns []goPattern
|
||||
}
|
||||
|
||||
func goDecode(d *idl) ([]byte, error) {
|
||||
var g goDecoder
|
||||
g.registry("decodeConnectionRequest", d.WS.Requests)
|
||||
g.registry("decodeRoomRequest", d.Requests)
|
||||
for _, e := range d.HTTP {
|
||||
if e.Request != nil {
|
||||
name := e.Request.name
|
||||
fmt.Fprintf(&g.b, "\nfunc Decode%s(body []byte) (%s, error) {\nreturn decode%s(body, \"\")\n}\n", name, name, name)
|
||||
}
|
||||
}
|
||||
for _, name := range d.typeNames() {
|
||||
if !d.inbound[name] {
|
||||
continue
|
||||
}
|
||||
if s := d.named[name]; s.Enum != nil {
|
||||
g.enum(name, s)
|
||||
} else {
|
||||
g.object(name, s)
|
||||
}
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
b.WriteString(goHeader)
|
||||
if len(g.patterns) == 0 {
|
||||
b.WriteString("\nimport \"encoding/json\"\n")
|
||||
} else {
|
||||
b.WriteString("\nimport (\n\"encoding/json\"\n\"regexp\"\n)\n\nvar (\n")
|
||||
for _, p := range g.patterns {
|
||||
fmt.Fprintf(&b, "%s = regexp.MustCompile(%s)\n", p.name, goRawString(p.expr))
|
||||
}
|
||||
b.WriteString(")\n")
|
||||
}
|
||||
b.Write(g.b.Bytes())
|
||||
return format.Source(b.Bytes())
|
||||
}
|
||||
|
||||
func (g *goDecoder) registry(fn string, messages []*message) {
|
||||
fmt.Fprintf(&g.b, "\nfunc %s(typ string, raw json.RawMessage, path string) (any, bool, error) {\nswitch typ {\n", fn)
|
||||
for _, m := range messages {
|
||||
fmt.Fprintf(&g.b, "case Request%s:\nv, err := decode%s(raw, path)\nreturn v, true, err\n", pascal(m.Type), m.Request.name)
|
||||
}
|
||||
g.b.WriteString("}\nreturn nil, false, nil\n}\n")
|
||||
}
|
||||
|
||||
func (g *goDecoder) enum(name string, s *schema) {
|
||||
consts := make([]string, len(s.Enum))
|
||||
for i, v := range s.Enum {
|
||||
consts[i] = name + pascal(v)
|
||||
}
|
||||
reason := "取值应为 " + strings.Join(s.Enum, "、") + " 之一"
|
||||
fmt.Fprintf(&g.b, "\nfunc decode%s(raw json.RawMessage, path string) (%s, error) {\ns, err := decodeString(raw, path)\nif err != nil {\nreturn \"\", err\n}\nswitch v := %s(s); v {\ncase %s:\nreturn v, nil\n}\nreturn \"\", invalid(path, %q)\n}\n",
|
||||
name, name, name, strings.Join(consts, ", "), reason)
|
||||
}
|
||||
|
||||
func (g *goDecoder) object(name string, s *schema) {
|
||||
fmt.Fprintf(&g.b, "\nfunc decode%s(raw json.RawMessage, path string) (%s, error) {\nvar v %s\n", name, name, name)
|
||||
fields := slices.Sorted(maps.Keys(s.Properties))
|
||||
if len(fields) == 0 {
|
||||
g.b.WriteString("_, err := decodeObject(raw, path)\nreturn v, err\n}\n")
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(&g.b, "m, err := decodeObject(raw, path, %s)\nif err != nil {\nreturn v, err\n}\n", goStrings(fields))
|
||||
if len(s.Required) > 0 {
|
||||
fmt.Fprintf(&g.b, "if err = requireFields(m, path, %s); err != nil {\nreturn v, err\n}\n", goStrings(slices.Sorted(slices.Values(s.Required))))
|
||||
}
|
||||
if s.OneOf != nil {
|
||||
groups := make([]string, len(s.OneOf))
|
||||
reasons := make([]string, len(s.OneOf))
|
||||
for i, alt := range s.OneOf {
|
||||
groups[i] = "[]string{" + goStrings(alt.Required) + "}"
|
||||
reasons[i] = strings.Join(alt.Required, "、")
|
||||
}
|
||||
optional := "nil"
|
||||
if names := optionalFields(s.OneOf[0]); len(names) > 0 {
|
||||
optional = "[]string{" + goStrings(names) + "}"
|
||||
}
|
||||
fmt.Fprintf(&g.b, "if err = checkOneOf(m, path, %q, %s, %s); err != nil {\nreturn v, err\n}\n",
|
||||
"只能提供以下字段组合之一:"+strings.Join(reasons, ";"), optional, strings.Join(groups, ", "))
|
||||
}
|
||||
for _, f := range fields {
|
||||
p := s.Properties[f]
|
||||
at := fmt.Sprintf("field(path, %q)", f)
|
||||
decode := "decodeString"
|
||||
switch {
|
||||
case p.Ref != "":
|
||||
decode = "decode" + refName(p.Ref)
|
||||
case p.Type == "integer":
|
||||
decode = "decodeInteger"
|
||||
}
|
||||
if slices.Contains(s.Required, f) {
|
||||
fmt.Fprintf(&g.b, "if v.%s, err = %s(m[%q], %s); err != nil {\nreturn v, err\n}\n", pascal(f), decode, f, at)
|
||||
g.checks("v."+pascal(f), at, p, name+pascal(f))
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&g.b, "if r, ok := m[%q]; ok {\nx, err := %s(r, %s)\nif err != nil {\nreturn v, err\n}\n", f, decode, at)
|
||||
g.checks("x", at, p, name+pascal(f))
|
||||
fmt.Fprintf(&g.b, "v.%s = &x\n}\n", pascal(f))
|
||||
}
|
||||
for _, entry := range s.AllOf {
|
||||
cond := entry.If.Required[0]
|
||||
value := *entry.If.Properties[cond].Const
|
||||
fmt.Fprintf(&g.b, "if v.%s == %s%s {\n", pascal(cond), refName(s.Properties[cond].Ref), pascal(value))
|
||||
for _, f := range slices.Sorted(maps.Keys(entry.Then.Properties)) {
|
||||
g.checks("v."+pascal(f), fmt.Sprintf("field(path, %q)", f), entry.Then.Properties[f], name+pascal(f)+"When"+pascal(value))
|
||||
}
|
||||
g.b.WriteString("}\n")
|
||||
}
|
||||
g.b.WriteString("return v, nil\n}\n")
|
||||
}
|
||||
|
||||
func (g *goDecoder) checks(value, at string, s *schema, patternName string) {
|
||||
check := func(fn, limit string) {
|
||||
fmt.Fprintf(&g.b, "if err = %s(%s, %s, %s); err != nil {\nreturn v, err\n}\n", fn, value, at, limit)
|
||||
}
|
||||
if s.MinLength != nil {
|
||||
check("checkMinLength", fmt.Sprint(*s.MinLength))
|
||||
}
|
||||
if s.MaxLength != nil {
|
||||
check("checkMaxLength", fmt.Sprint(*s.MaxLength))
|
||||
}
|
||||
if s.MaxBytes != nil {
|
||||
check("checkMaxBytes", fmt.Sprint(*s.MaxBytes))
|
||||
}
|
||||
if s.URL != nil {
|
||||
fmt.Fprintf(&g.b, "if err = checkURL(%s, %s); err != nil {\nreturn v, err\n}\n", value, at)
|
||||
}
|
||||
if s.Pattern != "" {
|
||||
name := "pattern" + patternName
|
||||
g.patterns = append(g.patterns, goPattern{name: name, expr: s.Pattern})
|
||||
check("checkPattern", name)
|
||||
}
|
||||
if s.Minimum != nil {
|
||||
check("checkMinimum", fmt.Sprint(*s.Minimum))
|
||||
}
|
||||
if s.Maximum != nil {
|
||||
check("checkMaximum", fmt.Sprint(*s.Maximum))
|
||||
}
|
||||
}
|
||||
|
||||
func goStrings(values []string) string {
|
||||
quoted := make([]string, len(values))
|
||||
for i, v := range values {
|
||||
quoted[i] = fmt.Sprintf("%q", v)
|
||||
}
|
||||
return strings.Join(quoted, ", ")
|
||||
}
|
||||
|
||||
func goRawString(s string) string {
|
||||
if strings.Contains(s, "`") {
|
||||
return fmt.Sprintf("%q", s)
|
||||
}
|
||||
return "`" + s + "`"
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const goHeader = "// Code generated by protocolgen from protocol/idl.json; DO NOT EDIT.\n\npackage protocol\n"
|
||||
|
||||
func goTypes(d *idl) ([]byte, error) {
|
||||
var body bytes.Buffer
|
||||
fmt.Fprintf(&body, "\nconst Version = %q\n", d.version)
|
||||
marshal := false
|
||||
for _, name := range d.typeNames() {
|
||||
s := d.named[name]
|
||||
if s.Enum != nil {
|
||||
writeGoEnum(&body, name, s)
|
||||
continue
|
||||
}
|
||||
writeGoStruct(&body, name, s)
|
||||
marshal = writeGoMarshal(&body, name, s) || marshal
|
||||
}
|
||||
body.WriteString("\ntype ErrorResponse struct {\nError ErrorInfo `json:\"error\"`\n}\n")
|
||||
writeGoMessageTypes(&body, d)
|
||||
writeGoCloseCodes(&body, d.WS.CloseCodes)
|
||||
writeGoRoutes(&body, d.HTTP)
|
||||
writeGoRoomRoutes(&body, d)
|
||||
writeGoHTTPStatus(&body, d)
|
||||
|
||||
var b bytes.Buffer
|
||||
b.WriteString(goHeader)
|
||||
if marshal {
|
||||
b.WriteString("\nimport \"encoding/json\"\n")
|
||||
}
|
||||
b.Write(body.Bytes())
|
||||
return format.Source(b.Bytes())
|
||||
}
|
||||
|
||||
func writeGoEnum(b *bytes.Buffer, name string, s *schema) {
|
||||
fmt.Fprintf(b, "\ntype %s string\n\nconst (\n", name)
|
||||
for _, v := range s.Enum {
|
||||
fmt.Fprintf(b, "%s%s %s = %q\n", name, pascal(v), name, v)
|
||||
}
|
||||
b.WriteString(")\n")
|
||||
}
|
||||
|
||||
func writeGoStruct(b *bytes.Buffer, name string, s *schema) {
|
||||
if len(s.Properties) == 0 {
|
||||
fmt.Fprintf(b, "\ntype %s struct{}\n", name)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(b, "\ntype %s struct {\n", name)
|
||||
for _, p := range slices.Sorted(maps.Keys(s.Properties)) {
|
||||
required := slices.Contains(s.Required, p)
|
||||
tag := p
|
||||
if !required {
|
||||
tag += ",omitempty"
|
||||
}
|
||||
fmt.Fprintf(b, "%s %s `json:%q`\n", pascal(p), goFieldType(s.Properties[p], required), tag)
|
||||
}
|
||||
b.WriteString("}\n")
|
||||
}
|
||||
|
||||
func goFieldType(s *schema, required bool) string {
|
||||
switch {
|
||||
case s.AnyOf != nil:
|
||||
return "*" + goValueType(s.AnyOf[0])
|
||||
case s.Type == "array":
|
||||
return "[]" + goValueType(s.Items)
|
||||
case !required:
|
||||
return "*" + goValueType(s)
|
||||
default:
|
||||
return goValueType(s)
|
||||
}
|
||||
}
|
||||
|
||||
func goValueType(s *schema) string {
|
||||
if s.Ref != "" {
|
||||
return refName(s.Ref)
|
||||
}
|
||||
return map[string]string{"string": "string", "integer": "int64", "number": "float64", "boolean": "bool"}[s.Type]
|
||||
}
|
||||
|
||||
func writeGoMarshal(b *bytes.Buffer, name string, s *schema) bool {
|
||||
var arrays []string
|
||||
for _, p := range slices.Sorted(maps.Keys(s.Properties)) {
|
||||
if s.Properties[p].Type == "array" {
|
||||
arrays = append(arrays, p)
|
||||
}
|
||||
}
|
||||
if len(arrays) == 0 {
|
||||
return false
|
||||
}
|
||||
fmt.Fprintf(b, "\nfunc (v %s) MarshalJSON() ([]byte, error) {\ntype plain %s\n", name, name)
|
||||
for _, p := range arrays {
|
||||
fmt.Fprintf(b, "if v.%s == nil {\nv.%s = []%s{}\n}\n", pascal(p), pascal(p), goValueType(s.Properties[p].Items))
|
||||
}
|
||||
b.WriteString("return json.Marshal(plain(v))\n}\n")
|
||||
return true
|
||||
}
|
||||
|
||||
func writeGoMessageTypes(b *bytes.Buffer, d *idl) {
|
||||
b.WriteString("\nconst (\n")
|
||||
for _, m := range slices.Concat(d.WS.Requests, d.Requests) {
|
||||
fmt.Fprintf(b, "Request%s = %q\n", pascal(m.Type), m.Type)
|
||||
}
|
||||
b.WriteString(")\n\nconst (\n")
|
||||
for _, e := range slices.Concat(d.WS.Events, d.Events) {
|
||||
fmt.Fprintf(b, "Event%s = %q\n", pascal(e.Type), e.Type)
|
||||
}
|
||||
b.WriteString(")\n")
|
||||
}
|
||||
|
||||
func writeGoCloseCodes(b *bytes.Buffer, codes []*closeCode) {
|
||||
if len(codes) == 0 {
|
||||
return
|
||||
}
|
||||
b.WriteString("\nconst (\n")
|
||||
for _, c := range codes {
|
||||
fmt.Fprintf(b, "Close%s = %d\n", pascal(c.Name), c.Code)
|
||||
}
|
||||
b.WriteString(")\n")
|
||||
}
|
||||
|
||||
func writeGoRoutes(b *bytes.Buffer, endpoints []*endpoint) {
|
||||
if len(endpoints) == 0 {
|
||||
return
|
||||
}
|
||||
b.WriteString("\nvar (\n")
|
||||
for _, e := range endpoints {
|
||||
fmt.Fprintf(b, "Route%s = Route{Pattern: %q, Auth: Auth%s, Status: %d%s}\n", pascal(e.Name), e.Method+" "+e.Path, pascal(e.Auth), e.status(), goLocation(e.Location))
|
||||
}
|
||||
b.WriteString(")\n")
|
||||
}
|
||||
|
||||
func goLocation(location bool) string {
|
||||
if location {
|
||||
return ", Location: true"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func writeGoRoomRoutes(b *bytes.Buffer, d *idl) {
|
||||
if len(d.Requests) == 0 {
|
||||
return
|
||||
}
|
||||
b.WriteString("\nvar RoomRoutes = []RoomRoute{\n")
|
||||
for _, m := range d.Requests {
|
||||
h := m.HTTP
|
||||
fmt.Fprintf(b, "{Route: Route{Pattern: %q, Auth: AuthTicket, Status: %d%s}, Type: Request%s", h.Method+" "+h.Path, h.status(), goLocation(h.Location), pascal(m.Type))
|
||||
params := pathParams(h.Path)
|
||||
if len(params) > 0 {
|
||||
fmt.Fprintf(b, ", params: []string{%s}", goStrings(params))
|
||||
}
|
||||
if h.Method != "GET" && h.Method != "DELETE" {
|
||||
b.WriteString(", body: true")
|
||||
} else if query := d.queryKinds(m.Request, params); len(query) > 0 {
|
||||
fmt.Fprintf(b, ", query: map[string]queryKind{%s}", strings.Join(query, ", "))
|
||||
}
|
||||
b.WriteString("},\n")
|
||||
}
|
||||
b.WriteString("}\n")
|
||||
}
|
||||
|
||||
func (d *idl) queryKinds(s *schema, params []string) []string {
|
||||
var kinds []string
|
||||
for _, name := range slices.Sorted(maps.Keys(s.Properties)) {
|
||||
if slices.Contains(params, name) {
|
||||
continue
|
||||
}
|
||||
kind := "queryString"
|
||||
if d.jsonType(s.Properties[name]) == "integer" {
|
||||
kind = "queryInteger"
|
||||
}
|
||||
kinds = append(kinds, fmt.Sprintf("%q: %s", name, kind))
|
||||
}
|
||||
return kinds
|
||||
}
|
||||
|
||||
func writeGoHTTPStatus(b *bytes.Buffer, d *idl) {
|
||||
for _, name := range d.typeNames() {
|
||||
s := d.named[name]
|
||||
if s.HTTPStatus == nil {
|
||||
continue
|
||||
}
|
||||
table := lowerFirst(name) + "HTTPStatus"
|
||||
fmt.Fprintf(b, "\nvar %s = map[%s]int{\n", table, name)
|
||||
for _, v := range s.Enum {
|
||||
if status, ok := s.HTTPStatus[v]; ok {
|
||||
fmt.Fprintf(b, "%s%s: %d,\n", name, pascal(v), status)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(b, "}\n\n// IDL 没有为某个取值指定 HTTP 状态码时返回 0。\nfunc (v %s) HTTPStatus() int {\nreturn %s[v]\n}\n", name, table)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type output struct {
|
||||
path string
|
||||
data []byte
|
||||
}
|
||||
|
||||
func main() {
|
||||
os.Exit(run(os.Args[1:], os.Stderr))
|
||||
}
|
||||
|
||||
func run(args []string, stderr io.Writer) int {
|
||||
flags := flag.NewFlagSet("protocolgen", flag.ContinueOnError)
|
||||
flags.SetOutput(stderr)
|
||||
root := flags.String("root", ".", "仓库根目录")
|
||||
check := flags.Bool("check", false, "只检查生成物是否与 IDL 一致,不写入文件")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 2
|
||||
}
|
||||
outputs, err := generateFrom(filepath.Join(*root, "protocol", "idl.json"))
|
||||
if err != nil {
|
||||
report(stderr, "%v", err)
|
||||
return 1
|
||||
}
|
||||
status := 0
|
||||
for _, out := range outputs {
|
||||
path := filepath.Join(*root, filepath.FromSlash(out.path))
|
||||
if *check {
|
||||
old, err := os.ReadFile(path)
|
||||
switch {
|
||||
case err != nil && !errors.Is(err, fs.ErrNotExist):
|
||||
report(stderr, "读取 %s:%v", out.path, err)
|
||||
status = 1
|
||||
case !bytes.Equal(old, out.data):
|
||||
report(stderr, "%s 不是最新的,运行 go run ./cmd/protocolgen 重新生成", out.path)
|
||||
status = 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := writeOutput(path, out.data); err != nil {
|
||||
report(stderr, "%v", err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
func report(stderr io.Writer, format string, args ...any) {
|
||||
_, _ = fmt.Fprintf(stderr, "protocolgen:"+format+"\n", args...)
|
||||
}
|
||||
|
||||
func generateFrom(path string) ([]output, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d, err := parseIDL(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := d.checkHandwritten(filepath.Dir(path)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return generate(d)
|
||||
}
|
||||
|
||||
// 生成函数只处理 check 接受过的 IDL,直接依赖它保证的结构,不再检查:引用都指向存在的类型(标量类型已经展开),数组都有 items,
|
||||
// anyOf 是 [类型, {"type": "null"}],allOf 的每个条件只按必填的枚举字段判断,需要解码函数的类型都已标记在 inbound 中。
|
||||
func generate(d *idl) ([]output, error) {
|
||||
types, err := goTypes(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decode, err := goDecode(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
api, err := openAPI(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outputs := []output{
|
||||
{"protocol/types.gen.go", types},
|
||||
{"protocol/decode.gen.go", decode},
|
||||
{"protocol/openapi.gen.json", api},
|
||||
{"web/src/core/realtime/protocol.gen.ts", tsProtocol(d)},
|
||||
{"web/src/core/api/http.gen.ts", tsHTTP(d)},
|
||||
}
|
||||
if len(d.urlDefs()) > 0 {
|
||||
outputs = append(outputs, output{"web/src/core/realtime/protocol.gen.test.ts", tsValidatorTests(d)})
|
||||
}
|
||||
return outputs, nil
|
||||
}
|
||||
|
||||
func writeOutput(path string, data []byte) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0o644)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
var initialisms = map[string]string{"id": "ID", "qq": "QQ", "url": "URL"}
|
||||
|
||||
func words(s string) []string {
|
||||
var out []string
|
||||
for _, part := range strings.FieldsFunc(s, func(r rune) bool { return r == '.' || r == '_' }) {
|
||||
start := 0
|
||||
for i := 1; i < len(part); i++ {
|
||||
if isUpper(part[i]) && !isUpper(part[i-1]) {
|
||||
out = append(out, part[start:i])
|
||||
start = i
|
||||
}
|
||||
}
|
||||
out = append(out, part[start:])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func isUpper(c byte) bool {
|
||||
return 'A' <= c && c <= 'Z'
|
||||
}
|
||||
|
||||
func pascal(s string) string {
|
||||
var b strings.Builder
|
||||
for _, w := range words(s) {
|
||||
b.WriteString(title(w))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func camel(s string) string {
|
||||
w := words(s)
|
||||
var b strings.Builder
|
||||
b.WriteString(strings.ToLower(w[0]))
|
||||
for _, x := range w[1:] {
|
||||
b.WriteString(title(x))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func title(w string) string {
|
||||
lower := strings.ToLower(w)
|
||||
if s, ok := initialisms[lower]; ok {
|
||||
return s
|
||||
}
|
||||
return strings.ToUpper(lower[:1]) + lower[1:]
|
||||
}
|
||||
|
||||
func lowerFirst(s string) string {
|
||||
return strings.ToLower(s[:1]) + s[1:]
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"maps"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func openAPI(d *idl) ([]byte, error) {
|
||||
schemas := map[string]any{
|
||||
"ErrorResponse": map[string]any{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": map[string]any{"error": componentRef("ErrorInfo")},
|
||||
"required": []string{"error"},
|
||||
},
|
||||
}
|
||||
for name, s := range d.Defs {
|
||||
if s.scalar() {
|
||||
continue
|
||||
}
|
||||
if err := addComponent(schemas, name, s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
paths := map[string]map[string]any{}
|
||||
for _, e := range d.HTTP {
|
||||
op := map[string]any{
|
||||
"operationId": e.Name,
|
||||
"responses": openAPIResponses(e.httpBinding, e.Response),
|
||||
}
|
||||
if e.Description != "" {
|
||||
op["summary"] = e.Description
|
||||
}
|
||||
if params := pathParams(e.Path); len(params) > 0 {
|
||||
list := make([]any, len(params))
|
||||
for i, p := range params {
|
||||
list[i] = map[string]any{"name": p, "in": "path", "required": true, "schema": map[string]any{"type": "string"}}
|
||||
}
|
||||
op["parameters"] = list
|
||||
}
|
||||
if e.Request != nil {
|
||||
op["requestBody"] = openAPIBody(e.Request)
|
||||
}
|
||||
security := []any{}
|
||||
if scheme := map[string]string{"member": "memberToken", "admin": "adminToken"}[e.Auth]; scheme != "" {
|
||||
security = append(security, map[string]any{scheme: []any{}})
|
||||
}
|
||||
op["security"] = security
|
||||
for _, s := range []*schema{e.Request, e.Response} {
|
||||
if err := addInlineComponent(schemas, s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if paths[e.Path] == nil {
|
||||
paths[e.Path] = map[string]any{}
|
||||
}
|
||||
paths[e.Path][strings.ToLower(e.Method)] = op
|
||||
}
|
||||
for _, m := range d.Requests {
|
||||
op, err := roomOperation(schemas, m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if paths[m.HTTP.Path] == nil {
|
||||
paths[m.HTTP.Path] = map[string]any{}
|
||||
}
|
||||
paths[m.HTTP.Path][strings.ToLower(m.HTTP.Method)] = op
|
||||
}
|
||||
|
||||
bearer := map[string]any{"type": "http", "scheme": "bearer"}
|
||||
ticket := map[string]any{"type": "http", "scheme": "bearer", "description": "进房票据或常驻房间的 API Key"}
|
||||
doc := map[string]any{
|
||||
"openapi": "3.1.0",
|
||||
"info": map[string]any{"title": d.Title, "version": d.version},
|
||||
"paths": paths,
|
||||
"components": map[string]any{
|
||||
"schemas": schemas,
|
||||
"securitySchemes": map[string]any{"memberToken": bearer, "ticket": ticket, "adminToken": bearer},
|
||||
},
|
||||
}
|
||||
var b bytes.Buffer
|
||||
enc := json.NewEncoder(&b)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func componentRef(name string) map[string]any {
|
||||
return map[string]any{"$ref": "#/components/schemas/" + name}
|
||||
}
|
||||
|
||||
func openAPIBody(s *schema) map[string]any {
|
||||
return map[string]any{
|
||||
"required": true,
|
||||
"content": map[string]any{"application/json": map[string]any{"schema": componentRef(s.dataType())}},
|
||||
}
|
||||
}
|
||||
|
||||
func openAPIResponses(b httpBinding, s *schema) map[string]any {
|
||||
success := map[string]any{"description": "成功"}
|
||||
if s != nil {
|
||||
success["content"] = map[string]any{"application/json": map[string]any{"schema": componentRef(s.dataType())}}
|
||||
}
|
||||
if b.Location {
|
||||
success["headers"] = map[string]any{"Location": map[string]any{"description": "新资源的地址", "schema": map[string]any{"type": "string"}}}
|
||||
}
|
||||
return map[string]any{
|
||||
strconv.Itoa(b.status()): success,
|
||||
"default": map[string]any{
|
||||
"description": "失败",
|
||||
"content": map[string]any{"application/json": map[string]any{"schema": componentRef("ErrorResponse")}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 路径参数不在查询串和请求体中;204 的请求没有响应体。
|
||||
func roomOperation(schemas map[string]any, m *message) (map[string]any, error) {
|
||||
b := *m.HTTP
|
||||
response := m.Response
|
||||
if b.status() == 204 {
|
||||
response = nil
|
||||
}
|
||||
op := map[string]any{
|
||||
"operationId": camel(m.Type),
|
||||
"responses": openAPIResponses(b, response),
|
||||
"security": []any{map[string]any{"ticket": []any{}}},
|
||||
}
|
||||
if m.Description != "" {
|
||||
op["summary"] = m.Description
|
||||
}
|
||||
if err := addInlineComponent(schemas, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pathNames := pathParams(b.Path)
|
||||
var params []any
|
||||
for _, name := range pathNames {
|
||||
s, err := openAPISchema(m.Request.Properties[name])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params = append(params, map[string]any{"name": name, "in": "path", "required": true, "schema": s})
|
||||
}
|
||||
if b.Method == "GET" || b.Method == "DELETE" {
|
||||
for _, name := range slices.Sorted(maps.Keys(m.Request.Properties)) {
|
||||
if slices.Contains(pathNames, name) {
|
||||
continue
|
||||
}
|
||||
s, err := openAPISchema(m.Request.Properties[name])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params = append(params, map[string]any{"name": name, "in": "query", "required": slices.Contains(m.Request.Required, name), "schema": s})
|
||||
}
|
||||
} else {
|
||||
body, err := roomBody(schemas, m.Request, pathNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
op["requestBody"] = body
|
||||
}
|
||||
if len(params) > 0 {
|
||||
op["parameters"] = params
|
||||
}
|
||||
return op, nil
|
||||
}
|
||||
|
||||
// 请求体没有字段时可以不带请求体。
|
||||
func roomBody(schemas map[string]any, s *schema, pathNames []string) (map[string]any, error) {
|
||||
var bodySchema any = componentRef(s.name)
|
||||
fields := len(s.Properties)
|
||||
if len(pathNames) == 0 {
|
||||
if err := addInlineComponent(schemas, s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
rest := *s
|
||||
rest.Properties = maps.Clone(s.Properties)
|
||||
for _, name := range pathNames {
|
||||
delete(rest.Properties, name)
|
||||
}
|
||||
rest.Required = slices.DeleteFunc(slices.Clone(s.Required), func(name string) bool { return slices.Contains(pathNames, name) })
|
||||
fields = len(rest.Properties)
|
||||
v, err := openAPISchema(&rest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodySchema = v
|
||||
}
|
||||
return map[string]any{
|
||||
"required": fields > 0,
|
||||
"content": map[string]any{"application/json": map[string]any{"schema": bodySchema}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func addInlineComponent(schemas map[string]any, s *schema) error {
|
||||
if s == nil || s.Ref != "" {
|
||||
return nil
|
||||
}
|
||||
return addComponent(schemas, s.name, s)
|
||||
}
|
||||
|
||||
func addComponent(schemas map[string]any, name string, s *schema) error {
|
||||
v, err := openAPISchema(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
schemas[name] = v
|
||||
return nil
|
||||
}
|
||||
|
||||
func openAPISchema(s *schema) (any, error) {
|
||||
data, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.UseNumber()
|
||||
var v any
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rewriteSchema(v), nil
|
||||
}
|
||||
|
||||
func rewriteSchema(v any) any {
|
||||
switch v := v.(type) {
|
||||
case map[string]any:
|
||||
for k, x := range v {
|
||||
if s, ok := x.(string); ok && k == "$ref" {
|
||||
v[k] = "#/components/schemas/" + refName(s)
|
||||
} else {
|
||||
v[k] = rewriteSchema(x)
|
||||
}
|
||||
}
|
||||
// 服务端接受非 ASCII 的主机名和路径,所以 format 是 iri 而不是 uri。
|
||||
if v["x-url"] == true {
|
||||
v["format"] = "iri"
|
||||
}
|
||||
case []any:
|
||||
for i, x := range v {
|
||||
v[i] = rewriteSchema(x)
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const tsHeader = "// Code generated by protocolgen from protocol/idl.json; DO NOT EDIT.\n"
|
||||
|
||||
func tsProtocol(d *idl) []byte {
|
||||
httpTypes := d.httpTypeNames()
|
||||
var b bytes.Buffer
|
||||
b.WriteString(tsHeader)
|
||||
fmt.Fprintf(&b, "\nexport const PROTOCOL_VERSION = '%s'\n", d.version)
|
||||
for _, name := range d.typeNames() {
|
||||
if !httpTypes[name] {
|
||||
writeTSType(&b, name, d.named[name])
|
||||
}
|
||||
}
|
||||
|
||||
requests := slices.Concat(d.WS.Requests, d.Requests)
|
||||
writeTSMap(&b, "RequestDataMap", requests, func(m *message) (string, *schema) { return m.Type, m.Request })
|
||||
writeTSMap(&b, "ResponseDataMap", requests, func(m *message) (string, *schema) { return m.Type, m.Response })
|
||||
writeTSMap(&b, "ConnectionEventDataMap", d.WS.Events, func(e *event) (string, *schema) { return e.Type, e.Data })
|
||||
writeTSMap(&b, "RoomEventDataMap", d.Events, func(e *event) (string, *schema) { return e.Type, e.Data })
|
||||
b.WriteString(tsEnvelopes)
|
||||
|
||||
b.WriteString("\nexport const CloseCode = {\n")
|
||||
for _, c := range d.WS.CloseCodes {
|
||||
fmt.Fprintf(&b, " %s: %d,\n", c.Name, c.Code)
|
||||
}
|
||||
b.WriteString("} as const\n")
|
||||
|
||||
b.WriteString("\nexport type Send = <T extends RequestType>(type: T, data: RequestDataMap[T]) => Promise<ResponseDataMap[T]>\n")
|
||||
b.WriteString("\nexport function roomRequests(send: Send) {\n return {\n")
|
||||
for _, m := range d.Requests {
|
||||
if len(m.Request.Properties) == 0 {
|
||||
fmt.Fprintf(&b, " %s: () => send('%s', {}),\n", camel(m.Type), m.Type)
|
||||
} else {
|
||||
fmt.Fprintf(&b, " %s: (data: %s) => send('%s', data),\n", camel(m.Type), m.Request.name, m.Type)
|
||||
}
|
||||
}
|
||||
b.WriteString(" }\n}\n")
|
||||
if names := d.urlDefs(); len(names) > 0 {
|
||||
b.WriteString(tsURLRule)
|
||||
for _, name := range names {
|
||||
writeTSValidator(&b, name, d.Defs[name])
|
||||
}
|
||||
}
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
// 与 protocol/decode.go 中的 validURL 是同一套规则,两边由 protocol/testdata 中共用的用例检查。
|
||||
const tsURLRule = `
|
||||
function validURL(value: string): boolean {
|
||||
if (/[\u0000-\u0020\u007F\\]/.test(value))
|
||||
return false
|
||||
const scheme = /^https?:\/\//i.exec(value)
|
||||
if (scheme === null)
|
||||
return false
|
||||
const rest = value.slice(scheme[0].length)
|
||||
const end = rest.search(/[/?#]/)
|
||||
const authority = end < 0 ? rest : rest.slice(0, end)
|
||||
const tail = end < 0 ? '' : rest.slice(end)
|
||||
let host = authority
|
||||
let port = ''
|
||||
if (authority.startsWith('[')) {
|
||||
const close = authority.indexOf(']') + 1
|
||||
host = authority.slice(0, close)
|
||||
if (close === 0 || !/^\[[0-9A-Fa-f:.]+\]$/.test(host))
|
||||
return false
|
||||
const after = authority.slice(close)
|
||||
if (after !== '') {
|
||||
if (!after.startsWith(':'))
|
||||
return false
|
||||
port = after.slice(1)
|
||||
}
|
||||
}
|
||||
else {
|
||||
const colon = authority.indexOf(':')
|
||||
if (colon >= 0) {
|
||||
host = authority.slice(0, colon)
|
||||
port = authority.slice(colon + 1)
|
||||
}
|
||||
if (!/^[\w.~\u0080-\u{10FFFF}-]+$/u.test(host))
|
||||
return false
|
||||
const labels = host.replace(/\.$/, '').split('.')
|
||||
if (/^(?:\d+|0x[0-9a-f]*)$/i.test(labels[labels.length - 1]) && !/^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/.test(host))
|
||||
return false
|
||||
}
|
||||
if (!/^\d*$/.test(port) || Number(port) > 65535)
|
||||
return false
|
||||
const hash = tail.indexOf('#')
|
||||
const path = (hash < 0 ? tail : tail.slice(0, hash)).split('?')[0]
|
||||
const fragment = hash < 0 ? '' : tail.slice(hash + 1)
|
||||
const badEscape = /%(?![0-9A-Fa-f]{2})/
|
||||
if (badEscape.test(path) || badEscape.test(fragment))
|
||||
return false
|
||||
try {
|
||||
return new URL(value).hostname !== ''
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func writeTSValidator(b *bytes.Buffer, name string, s *schema) {
|
||||
var checks []string
|
||||
if s.MinLength != nil {
|
||||
checks = append(checks, fmt.Sprintf("[...value].length >= %d", *s.MinLength))
|
||||
}
|
||||
if s.MaxLength != nil {
|
||||
checks = append(checks, fmt.Sprintf("[...value].length <= %d", *s.MaxLength))
|
||||
}
|
||||
if s.MaxBytes != nil {
|
||||
checks = append(checks, fmt.Sprintf("new TextEncoder().encode(value).length <= %d", *s.MaxBytes))
|
||||
}
|
||||
checks = append(checks, "validURL(value)")
|
||||
fmt.Fprintf(b, "\nexport function is%s(value: string): boolean {\n return %s\n}\n", name, strings.Join(checks, " && "))
|
||||
}
|
||||
|
||||
func tsValidatorTests(d *idl) []byte {
|
||||
names := d.urlDefs()
|
||||
validators := make([]string, len(names))
|
||||
for i, name := range names {
|
||||
validators[i] = "is" + name
|
||||
}
|
||||
var b bytes.Buffer
|
||||
b.WriteString(tsHeader)
|
||||
fmt.Fprintf(&b, "\nimport { readFileSync } from 'node:fs'\nimport { describe, expect, it } from 'vitest'\nimport { %s } from './protocol.gen'\n", strings.Join(validators, ", "))
|
||||
b.WriteString(`
|
||||
function cases(name: string): { value: string, valid: boolean }[] {
|
||||
return JSON.parse(readFileSync(new URL(` + "`../../../../protocol/testdata/${name}.json`" + `, import.meta.url), 'utf8')) as { value: string, valid: boolean }[]
|
||||
}
|
||||
`)
|
||||
for _, name := range names {
|
||||
fmt.Fprintf(&b, "\ndescribe('is%s', () => {\n it('对 protocol/testdata/%s.json 中的每个用例与服务端的判断相同', () => {\n expect(cases('%s').filter(c => is%s(c.value) !== c.valid).map(c => c.value)).toEqual([])\n })\n})\n", name, name, name, name)
|
||||
}
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
const tsEnvelopes = `
|
||||
export type RequestType = keyof RequestDataMap
|
||||
export type ConnectionEventType = keyof ConnectionEventDataMap
|
||||
export type RoomEventType = keyof RoomEventDataMap
|
||||
|
||||
export interface RequestMessage<T extends RequestType = RequestType> {
|
||||
kind: 'request'
|
||||
type: T
|
||||
id: string
|
||||
data: RequestDataMap[T]
|
||||
}
|
||||
|
||||
export type ResponseMessage<T extends RequestType = RequestType> = {
|
||||
[K in T]: { kind: 'response', type: K, id: string } & (
|
||||
| { data: ResponseDataMap[K], error?: undefined }
|
||||
| { data: Empty, error: ErrorInfo }
|
||||
)
|
||||
}[T]
|
||||
|
||||
export type ConnectionEventMessage<T extends ConnectionEventType = ConnectionEventType> = {
|
||||
[K in T]: { kind: 'event', type: K, data: ConnectionEventDataMap[K] }
|
||||
}[T]
|
||||
|
||||
export type RoomEventMessage<T extends RoomEventType = RoomEventType> = {
|
||||
[K in T]: { kind: 'event', type: K, term: number, seq: number, data: RoomEventDataMap[K] }
|
||||
}[T]
|
||||
|
||||
export type ServerMessage = ResponseMessage | ConnectionEventMessage | RoomEventMessage
|
||||
`
|
||||
|
||||
func tsHTTP(d *idl) []byte {
|
||||
httpTypes := d.httpTypeNames()
|
||||
var body bytes.Buffer
|
||||
imports := map[string]bool{"ErrorInfo": true}
|
||||
for _, name := range slices.Sorted(maps.Keys(httpTypes)) {
|
||||
s := d.named[name]
|
||||
writeTSType(&body, name, s)
|
||||
for _, p := range s.Properties {
|
||||
if ref := tsRef(p); ref != "" {
|
||||
imports[ref] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
body.WriteString(tsHTTPRuntime)
|
||||
body.WriteString("\nexport function httpClient(send: HttpSend) {\n return {\n")
|
||||
for _, e := range d.HTTP {
|
||||
response := "void"
|
||||
if e.Response != nil {
|
||||
response = e.Response.dataType()
|
||||
if e.Response.Ref != "" {
|
||||
imports[response] = true
|
||||
}
|
||||
}
|
||||
var params []string
|
||||
path := e.Path
|
||||
for _, p := range pathParams(e.Path) {
|
||||
params = append(params, p+": string")
|
||||
path = strings.Replace(path, "{"+p+"}", "${encodeURIComponent("+p+")}", 1)
|
||||
}
|
||||
quoted := "'" + path + "'"
|
||||
if strings.Contains(path, "${") {
|
||||
quoted = "`" + path + "`"
|
||||
}
|
||||
requestBody := ""
|
||||
if e.Request != nil {
|
||||
if len(e.Request.Properties) == 0 {
|
||||
requestBody = ", body: {}"
|
||||
} else {
|
||||
params = append(params, "body: "+e.Request.name)
|
||||
requestBody = ", body"
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&body, " %s: (%s) => send<%s>({ method: '%s', path: %s, auth: '%s'%s }),\n", e.Name, strings.Join(params, ", "), response, e.Method, quoted, e.Auth, requestBody)
|
||||
}
|
||||
body.WriteString(" }\n}\n")
|
||||
|
||||
var b bytes.Buffer
|
||||
b.WriteString(tsHeader)
|
||||
fmt.Fprintf(&b, "\nimport type { %s } from '../realtime/protocol.gen'\n", strings.Join(slices.Sorted(maps.Keys(imports)), ", "))
|
||||
b.Write(body.Bytes())
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
const tsHTTPRuntime = `
|
||||
export interface ErrorResponse {
|
||||
error: ErrorInfo
|
||||
}
|
||||
|
||||
export type Auth = 'none' | 'member' | 'admin'
|
||||
|
||||
export interface HttpRequest {
|
||||
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
||||
path: string
|
||||
auth: Auth
|
||||
body?: unknown
|
||||
}
|
||||
|
||||
export type HttpSend = <T>(request: HttpRequest) => Promise<T>
|
||||
`
|
||||
|
||||
func (d *idl) httpTypeNames() map[string]bool {
|
||||
names := map[string]bool{}
|
||||
for _, e := range d.HTTP {
|
||||
for _, s := range []*schema{e.Request, e.Response} {
|
||||
if s != nil && s.Ref == "" {
|
||||
names[s.name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func writeTSMap[T any](b *bytes.Buffer, name string, items []T, entry func(T) (string, *schema)) {
|
||||
fmt.Fprintf(b, "\nexport interface %s {\n", name)
|
||||
for _, item := range items {
|
||||
key, s := entry(item)
|
||||
fmt.Fprintf(b, " '%s': %s\n", key, s.dataType())
|
||||
}
|
||||
b.WriteString("}\n")
|
||||
}
|
||||
|
||||
func writeTSType(b *bytes.Buffer, name string, s *schema) {
|
||||
switch {
|
||||
case s.Enum != nil:
|
||||
values := make([]string, len(s.Enum))
|
||||
for i, v := range s.Enum {
|
||||
values[i] = "'" + v + "'"
|
||||
}
|
||||
fmt.Fprintf(b, "\nexport type %s = %s\n", name, strings.Join(values, " | "))
|
||||
case len(s.Properties) == 0:
|
||||
fmt.Fprintf(b, "\nexport type %s = Record<string, never>\n", name)
|
||||
case s.OneOf != nil:
|
||||
optional := optionalFields(s.OneOf[0])
|
||||
alternatives := make([]string, len(s.OneOf))
|
||||
for i, alt := range s.OneOf {
|
||||
fields := make([]string, 0, len(alt.Required)+len(optional))
|
||||
for _, f := range alt.Required {
|
||||
fields = append(fields, f+": "+tsFieldType(s.Properties[f]))
|
||||
}
|
||||
for _, f := range optional {
|
||||
fields = append(fields, f+"?: "+tsFieldType(s.Properties[f]))
|
||||
}
|
||||
alternatives[i] = "{ " + strings.Join(fields, ", ") + " }"
|
||||
}
|
||||
fmt.Fprintf(b, "\nexport type %s = %s\n", name, strings.Join(alternatives, " | "))
|
||||
default:
|
||||
fmt.Fprintf(b, "\nexport interface %s {\n", name)
|
||||
for _, p := range slices.Sorted(maps.Keys(s.Properties)) {
|
||||
optional := ""
|
||||
if !slices.Contains(s.Required, p) {
|
||||
optional = "?"
|
||||
}
|
||||
fmt.Fprintf(b, " %s%s: %s\n", p, optional, tsFieldType(s.Properties[p]))
|
||||
}
|
||||
b.WriteString("}\n")
|
||||
}
|
||||
}
|
||||
|
||||
func tsFieldType(s *schema) string {
|
||||
switch {
|
||||
case s.AnyOf != nil:
|
||||
return tsValueType(s.AnyOf[0]) + " | null"
|
||||
case s.Type == "array":
|
||||
return tsValueType(s.Items) + "[]"
|
||||
default:
|
||||
return tsValueType(s)
|
||||
}
|
||||
}
|
||||
|
||||
func tsValueType(s *schema) string {
|
||||
if s.Ref != "" {
|
||||
return refName(s.Ref)
|
||||
}
|
||||
return map[string]string{"string": "string", "integer": "number", "number": "number", "boolean": "boolean"}[s.Type]
|
||||
}
|
||||
|
||||
func tsRef(s *schema) string {
|
||||
switch {
|
||||
case s.AnyOf != nil:
|
||||
return refName(s.AnyOf[0].Ref)
|
||||
case s.Type == "array":
|
||||
return refName(s.Items.Ref)
|
||||
default:
|
||||
return refName(s.Ref)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user