返回 DeepSeek-Reasonix
schema.go
根目录 / internal / extension / protocol / schema.go
1 package protocol
2
3 import (
4 "encoding/json"
5 "fmt"
6 "reflect"
7 "sort"
8 "strconv"
9 "strings"
10 "sync"
11 )
12
13 // SchemaDraft202012 is the JSON Schema dialect of the generated document.
14 const SchemaDraft202012 = "https://json-schema.org/draft/2020-12/schema"
15
16 // SchemaTitle is the generated document's human title.
17 const SchemaTitle = "Reasonix Extension Protocol v1"
18
19 var rawMessageType = reflect.TypeOf(json.RawMessage{})
20
21 // BuildSchemaDocument reflection-walks the frozen registry and produces the
22 // canonical JSON Schema (draft 2020-12) document: one methods object keyed by
23 // sorted method name and one $defs entry per wire DTO. The returned map
24 // marshals deterministically because encoding/json sorts map keys.
25 func BuildSchemaDocument() (map[string]any, error) {
26 defs := map[string]any{}
27 methods := map[string]any{}
28 for _, spec := range Registry() {
29 paramsRef, err := buildJSONSchema(defs, spec.ParamsType)
30 if err != nil {
31 return nil, fmt.Errorf("%s params: %w", spec.Name, err)
32 }
33 var result any
34 if spec.Notification() {
35 result = nil
36 } else {
37 result, err = buildJSONSchema(defs, spec.ResultType)
38 if err != nil {
39 return nil, fmt.Errorf("%s result: %w", spec.Name, err)
40 }
41 }
42 methods[string(spec.Name)] = map[string]any{
43 "direction": string(spec.Direction),
44 "class": string(spec.Class),
45 "params": paramsRef,
46 "result": result,
47 "notification": spec.Notification(),
48 }
49 }
50
51 limits := FrozenLimits()
52 errors := make([]any, 0, len(frozenErrorSpecs))
53 for _, contract := range ErrorContracts() {
54 errors = append(errors, map[string]any{
55 "reason": string(contract.Reason),
56 "jsonRpcCode": contract.JSONRPCCode,
57 "message": contract.Message,
58 "retryable": contract.Retryable,
59 })
60 }
61 interceptEvents := InterceptEvents()
62 events := make([]any, len(interceptEvents))
63 for i, event := range interceptEvents {
64 events[i] = event
65 }
66
67 return map[string]any{
68 "$schema": SchemaDraft202012,
69 "$id": ProtocolID,
70 "title": SchemaTitle,
71 "protocol": ProtocolID,
72 "protocolID": ProtocolID,
73 "protocolMajor": ProtocolMajor,
74 "limits": map[string]any{
75 "frameBytes": limits.FrameBytes,
76 "externalizeFieldBytes": limits.ExternalizeFieldBytes,
77 "contentRefChunkBytes": limits.ContentRefChunkBytes,
78 "contentRefObjectBytes": limits.ContentRefObjectBytes,
79 },
80 "interceptEvents": events,
81 "errors": errors,
82 "methods": methods,
83 "$defs": defs,
84 }, nil
85 }
86
87 // buildJSONSchema maps a Go wire type to a JSON Schema. Named structs become
88 // $defs entries referenced by name; unconstrained JSON (json.RawMessage, any)
89 // becomes the boolean schema true.
90 func buildJSONSchema(defs map[string]any, typ reflect.Type) (any, error) {
91 for typ.Kind() == reflect.Pointer {
92 typ = typ.Elem()
93 }
94 if typ == rawMessageType {
95 return true, nil
96 }
97 if allowed, ok := enumTypes[typ]; ok {
98 values := append([]string(nil), allowed...)
99 sort.Strings(values)
100 enum := make([]any, len(values))
101 for i, value := range values {
102 enum[i] = value
103 }
104 return map[string]any{"type": "string", "enum": enum}, nil
105 }
106 switch typ.Kind() {
107 case reflect.Struct:
108 name := typ.Name()
109 if name == "" {
110 return nil, fmt.Errorf("anonymous struct %v is not a named wire DTO", typ)
111 }
112 if _, registered := defs[name]; !registered {
113 // Reserve the name before walking fields so self-referencing DTOs
114 // terminate instead of recursing forever.
115 defs[name] = true
116 object, err := buildObjectSchema(defs, typ)
117 if err != nil {
118 return nil, fmt.Errorf("$defs.%s: %w", name, err)
119 }
120 defs[name] = object
121 }
122 return map[string]any{"$ref": "#/$defs/" + name}, nil
123 case reflect.Slice, reflect.Array:
124 if typ.Elem().Kind() == reflect.Uint8 {
125 return map[string]any{"type": "string"}, nil
126 }
127 items, err := buildJSONSchema(defs, typ.Elem())
128 if err != nil {
129 return nil, err
130 }
131 return map[string]any{"type": "array", "items": items}, nil
132 case reflect.String:
133 return map[string]any{"type": "string"}, nil
134 case reflect.Bool:
135 return map[string]any{"type": "boolean"}, nil
136 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
137 reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
138 return map[string]any{"type": "integer"}, nil
139 case reflect.Float32, reflect.Float64:
140 return map[string]any{"type": "number"}, nil
141 case reflect.Map:
142 if typ.Key().Kind() != reflect.String {
143 return nil, fmt.Errorf("unsupported wire map key type %v", typ.Key())
144 }
145 additional, err := buildJSONSchema(defs, typ.Elem())
146 if err != nil {
147 return nil, err
148 }
149 return map[string]any{"type": "object", "additionalProperties": additional}, nil
150 case reflect.Interface:
151 if typ.NumMethod() == 0 {
152 return true, nil
153 }
154 }
155 return nil, fmt.Errorf("unsupported wire type %v", typ)
156 }
157
158 // buildObjectSchema renders one named DTO struct as a closed JSON Schema
159 // object: properties sorted (by map marshal), required from omitempty
160 // analysis, additionalProperties:false, plus tag-derived minLength and
161 // minimum/maximum constraints.
162 func buildObjectSchema(defs map[string]any, typ reflect.Type) (map[string]any, error) {
163 properties := map[string]any{}
164 var required []string
165 for i := 0; i < typ.NumField(); i++ {
166 field := typ.Field(i)
167 if field.PkgPath != "" {
168 continue
169 }
170 name, omitEmpty, skip := jsonField(field)
171 if skip {
172 continue
173 }
174 if field.Anonymous && name == "" {
175 return nil, fmt.Errorf("embedded field %v is not supported in wire DTOs", field.Type)
176 }
177 schema, err := buildJSONSchema(defs, field.Type)
178 if err != nil {
179 return nil, fmt.Errorf("field %s: %w", name, err)
180 }
181 schema = applyFieldTags(schema, field)
182 properties[name] = schema
183 if !omitEmpty {
184 required = append(required, name)
185 }
186 }
187 for i := 1; i < len(required); i++ {
188 if required[i-1] == required[i] {
189 return nil, fmt.Errorf("duplicate JSON field %q", required[i])
190 }
191 }
192 sort.Strings(required)
193 object := map[string]any{
194 "type": "object",
195 "additionalProperties": false,
196 "properties": properties,
197 }
198 if len(required) > 0 {
199 object["required"] = required
200 }
201 return object, nil
202 }
203
204 // applyFieldTags folds the validate/externalizable struct tags into JSON
205 // Schema constraints: nonempty → minLength, min=/max= → minimum/maximum,
206 // externalizable → the x-externalizable annotation.
207 func applyFieldTags(schema any, field reflect.StructField) any {
208 object, ok := schema.(map[string]any)
209 if !ok {
210 // Unconstrained JSON (json.RawMessage, any) is the boolean schema
211 // true; an externalizable tag still needs its annotation, so upgrade
212 // to an annotation-only object schema, which accepts the same values.
213 if externalizable(field) {
214 return map[string]any{"x-externalizable": true}
215 }
216 return schema
217 }
218 for _, tag := range strings.Split(field.Tag.Get("validate"), ",") {
219 switch {
220 case tag == "nonempty":
221 if object["type"] == "string" {
222 object["minLength"] = 1
223 }
224 case strings.HasPrefix(tag, "min="):
225 if minimum, err := strconv.ParseFloat(strings.TrimPrefix(tag, "min="), 64); err == nil {
226 object["minimum"] = minimum
227 }
228 case strings.HasPrefix(tag, "max="):
229 if maximum, err := strconv.ParseFloat(strings.TrimPrefix(tag, "max="), 64); err == nil {
230 object["maximum"] = maximum
231 }
232 }
233 }
234 if externalizable(field) {
235 object["x-externalizable"] = true
236 }
237 return object
238 }
239
240 func externalizable(field reflect.StructField) bool {
241 return field.Tag.Get("externalizable") == "true"
242 }
243
244 var (
245 schemaOnce sync.Once
246 schemaBytes []byte
247 schemaErr error
248 )
249
250 // CanonicalSchemaBytes is the deterministic byte form of BuildSchemaDocument:
251 // one compact JSON document, identical across runs and processes.
252 func CanonicalSchemaBytes() ([]byte, error) {
253 schemaOnce.Do(func() {
254 document, err := BuildSchemaDocument()
255 if err != nil {
256 schemaErr = err
257 return
258 }
259 schemaBytes, schemaErr = json.Marshal(document)
260 })
261 if schemaErr != nil {
262 return nil, schemaErr
263 }
264 return append([]byte(nil), schemaBytes...), nil
265 }
266
267 // SchemaHash returns the committed SHA-256 of the canonical schema document.
268 // Handshake comparisons use it to prove both peers run the identical frozen
269 // contract.
270 func SchemaHash() string {
271 return GeneratedSchemaHash
272 }
273
273 lines GO