Type the REST API's responses
Every tool now returns a Go struct (apitypes.go) instead of a map, and typeschema.go derives the OpenAPI response schemas from those types by reflection (json, doc and enum tags), so the document cannot drift from what is sent. Text results are documented as text/plain, settings in answers use the typed settings schema, and edits answer with EditResponse. TestResponsesMatchTheirSchemas checks real responses against the generated schemas. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The REST API's responses are Go structs (apitypes.go); their JSON Schemas
|
||||
// in the OpenAPI document are derived from those types, so what is documented
|
||||
// is what is sent. Struct tags: `json` as usual (omitempty = optional), `doc`
|
||||
// is the description, `enum` a comma-separated list of the allowed strings.
|
||||
// Named structs become components/schemas.
|
||||
|
||||
type schemaBuilder struct {
|
||||
defs map[string]any
|
||||
}
|
||||
|
||||
func newSchemaBuilder() *schemaBuilder { return &schemaBuilder{defs: map[string]any{}} }
|
||||
|
||||
var timeType = reflect.TypeOf(time.Time{})
|
||||
|
||||
// of returns the schema of t (a $ref for a named struct).
|
||||
func (b *schemaBuilder) of(t reflect.Type) map[string]any {
|
||||
switch t.Kind() {
|
||||
case reflect.Pointer:
|
||||
return nullable(b.of(t.Elem()))
|
||||
case reflect.Bool:
|
||||
return map[string]any{"type": "boolean"}
|
||||
case reflect.String:
|
||||
return map[string]any{"type": "string"}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return map[string]any{"type": "integer"}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return map[string]any{"type": "number"}
|
||||
case reflect.Slice, reflect.Array:
|
||||
return map[string]any{"type": "array", "items": b.of(t.Elem())}
|
||||
case reflect.Map:
|
||||
return map[string]any{"type": "object", "additionalProperties": b.of(t.Elem())}
|
||||
case reflect.Struct:
|
||||
if t == timeType {
|
||||
return map[string]any{"type": "string", "format": "date-time"}
|
||||
}
|
||||
if t.Name() == "" {
|
||||
return b.object(t)
|
||||
}
|
||||
if _, ok := b.defs[t.Name()]; !ok {
|
||||
b.defs[t.Name()] = nil // against recursion
|
||||
b.defs[t.Name()] = b.object(t)
|
||||
}
|
||||
return map[string]any{"$ref": "#/components/schemas/" + t.Name()}
|
||||
}
|
||||
return map[string]any{} // interfaces: any value
|
||||
}
|
||||
|
||||
func (b *schemaBuilder) object(t reflect.Type) map[string]any {
|
||||
props := map[string]any{}
|
||||
var required []string
|
||||
for i := range t.NumField() {
|
||||
f := t.Field(i)
|
||||
name, opts, _ := strings.Cut(f.Tag.Get("json"), ",")
|
||||
if !f.IsExported() || name == "-" {
|
||||
continue
|
||||
}
|
||||
if name == "" {
|
||||
name = f.Name
|
||||
}
|
||||
s := b.of(f.Type)
|
||||
if d := f.Tag.Get("doc"); d != "" {
|
||||
s["description"] = d
|
||||
}
|
||||
if e := f.Tag.Get("enum"); e != "" {
|
||||
s["enum"] = strings.Split(e, ",")
|
||||
}
|
||||
props[name] = s
|
||||
if !strings.Contains(","+opts+",", ",omitempty,") {
|
||||
required = append(required, name)
|
||||
}
|
||||
}
|
||||
out := map[string]any{"type": "object", "properties": props}
|
||||
if len(required) > 0 {
|
||||
out["required"] = required
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// nullable lets a schema also be null.
|
||||
func nullable(s map[string]any) map[string]any {
|
||||
if typ, ok := s["type"].(string); ok {
|
||||
out := map[string]any{}
|
||||
for k, v := range s {
|
||||
out[k] = v
|
||||
}
|
||||
out["type"] = []string{typ, "null"}
|
||||
return out
|
||||
}
|
||||
return map[string]any{"anyOf": []any{s, map[string]any{"type": "null"}}}
|
||||
}
|
||||
Reference in New Issue
Block a user