Files

107 lines
5.7 KiB
Go
Raw Permalink Normal View History

// Regenerates the CALL-MEMORY extension from the public Go request models.
package main
import (
"data-terminal-cloud/internal/callmemory"
"encoding/json"
"os"
"reflect"
"strings"
"time"
)
func schema(t reflect.Type) map[string]any {
if t.Kind() == reflect.Pointer {
return map[string]any{"anyOf": []any{schema(t.Elem()), map[string]any{"type": "null"}}}
}
if t == reflect.TypeOf(time.Time{}) {
return map[string]any{"type": "string", "format": "date-time"}
}
if t == reflect.TypeOf(json.RawMessage{}) {
return map[string]any{}
}
switch t.Kind() {
case reflect.String:
return map[string]any{"type": "string"}
case reflect.Bool:
return map[string]any{"type": "boolean"}
case reflect.Int, reflect.Int64:
return map[string]any{"type": "integer", "format": "int64", "minimum": 0}
case reflect.Slice:
return map[string]any{"type": "array", "items": schema(t.Elem())}
case reflect.Map:
return map[string]any{"type": "object", "additionalProperties": schema(t.Elem())}
case reflect.Struct:
props := map[string]any{}
required := []string{}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
tag := strings.Split(f.Tag.Get("json"), ",")
if tag[0] == "-" {
continue
}
if f.Anonymous {
nested := schema(f.Type)
for k, v := range nested["properties"].(map[string]any) {
props[k] = v
}
required = append(required, nested["required"].([]string)...)
continue
}
name := tag[0]
if name == "" {
name = f.Name
}
props[name] = schema(f.Type)
if !strings.Contains(f.Tag.Get("json"), "omitempty") {
required = append(required, name)
}
}
return map[string]any{"type": "object", "additionalProperties": false, "properties": props, "required": required}
default:
return map[string]any{}
}
}
func main() {
device := map[string]any{"warmup": callmemory.WarmupInput{}, "prepare": callmemory.PrepareInput{}, "events": callmemory.EventsInput{}, "heartbeat": callmemory.HeartbeatInput{}, "end": callmemory.EndInput{}, "status": callmemory.StatusInput{}, "command-acks": callmemory.AckInput{}}
memory := map[string]any{"query": callmemory.QueryInput{}, "mutations": callmemory.MutationInput{}, "mutations/resolve": callmemory.ResolveInput{}, "mutations/confirm": callmemory.ConfirmInput{}, "mutations/status": struct {
ID string `json:"mutation_id"`
}{}, "sync/status": struct {
ID string `json:"elder_id"`
}{}, "access/query": struct {
Elder string `json:"elder_id"`
}{}, "access/grant": callmemory.AccessInput{}}
paths := map[string]any{}
schemas := map[string]any{"Manifest": schema(reflect.TypeOf(callmemory.Manifest{}))}
for family, ops := range map[string]map[string]any{"device/calls": device, "memories": memory} {
for name, model := range ops {
key := strings.ReplaceAll(family+"/"+name, "/", "_")
security := "CustomerBearer"
description := "登录用户,独立记忆权限校验。正文上限 64 KiB,拒绝重复 JSON 键和未知字段。变更需提交稳定 mutation_id 与目标 record_version。"
parameters := []any{map[string]any{"in": "header", "name": "X-Request-ID", "required": family == "device/calls", "schema": map[string]any{"type": "string", "format": "uuid"}}}
if family == "device/calls" {
security = "DeviceHMAC"
description = "ONBOARDING-1.1 DEVREQ1 签名原始正文。nonce 5 分钟去重,时钟偏差不超过 120 秒。事件和 status 不续租;heartbeat 必须在启动 SDK 前成功,后续每 5 秒续租,绝对期限最长 45 秒。"
for _, header := range []string{"X-Device-ID", "X-Key-Version", "X-Timestamp-Ms", "X-Nonce"} {
parameters = append(parameters, map[string]any{"in": "header", "name": header, "required": true, "schema": map[string]any{"type": "string"}})
}
}
schemas[key] = schema(reflect.TypeOf(model))
responses := map[string]any{}
for _, status := range []string{"200", "400", "401", "403", "404", "409", "410", "413", "422", "429", "503"} {
responses[status] = map[string]any{"description": "CALL-MEMORY envelope; code/recovery_action are stable, data is present on success", "content": map[string]any{"application/json": map[string]any{"schema": map[string]any{"$ref": "#/components/schemas/Envelope"}}}}
}
paths["/api/v1/"+family+"/"+name] = map[string]any{"post": map[string]any{"operationId": key, "description": description, "security": []any{map[string]any{security: []string{}}}, "parameters": parameters, "requestBody": map[string]any{"required": true, "content": map[string]any{"application/json": map[string]any{"schema": map[string]any{"$ref": "#/components/schemas/" + key}}}}, "responses": responses}}
}
}
schemas["Envelope"] = map[string]any{"type": "object", "required": []string{"ok", "code", "request_id", "server_time_ms"}, "properties": map[string]any{"ok": map[string]any{"type": "boolean"}, "code": map[string]any{"type": "string"}, "request_id": map[string]any{"type": "string"}, "server_time_ms": map[string]any{"type": "integer"}, "message": map[string]any{"type": "string"}, "recovery_action": map[string]any{"type": "string"}, "retry_after_ms": map[string]any{"type": "integer"}, "data": map[string]any{"type": "object", "additionalProperties": true}}}
doc := map[string]any{"openapi": "3.1.0", "info": map[string]any{"title": "CALL-MEMORY-1.1", "version": "1.1.0"}, "paths": paths, "components": map[string]any{"schemas": schemas, "securitySchemes": map[string]any{"DeviceHMAC": map[string]any{"type": "apiKey", "in": "header", "name": "Authorization", "description": "Device-HMAC-SHA256 <hex HMAC-SHA256 of DEVREQ1 canonical bytes>"}, "CustomerBearer": map[string]any{"type": "http", "scheme": "bearer"}}}}
b, e := json.MarshalIndent(doc, "", " ")
if e != nil {
panic(e)
}
if e = os.WriteFile("openapi/call-memory.yaml", append(b, '\n'), 0644); e != nil {
panic(e)
}
}