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:
@@ -128,11 +128,9 @@ func (a *app) areaHandler(method string) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
if method != http.MethodGet {
|
||||
if m, ok := res.(map[string]any); ok {
|
||||
if e, ok := m["etag"].(string); ok {
|
||||
w.Header().Set("ETag", `"`+e+`"`)
|
||||
a.sys.noteTokenEdit(tok.Name, e)
|
||||
}
|
||||
if r, ok := res.(EditResponse); ok {
|
||||
w.Header().Set("ETag", `"`+r.ETag+`"`)
|
||||
a.sys.noteTokenEdit(tok.Name, r.ETag)
|
||||
}
|
||||
} else if data, err := os.ReadFile(a.sys.settingsPath()); err == nil {
|
||||
w.Header().Set("ETag", `"`+etag(data)+`"`)
|
||||
@@ -216,7 +214,7 @@ func (a *app) area(method string, segs []string, area map[string]any, body []byt
|
||||
|
||||
// areaOps lists the paths of the areas for the OpenAPI document: the options
|
||||
// to a depth of three (router/lans/{name}); deeper paths work the same.
|
||||
func areaPaths(schema map[string]any) map[string]any {
|
||||
func areaPaths(schema, edited map[string]any) map[string]any {
|
||||
paths := map[string]any{}
|
||||
var emit func(parts []string, s map[string]any)
|
||||
emit = func(parts []string, s map[string]any) {
|
||||
@@ -251,14 +249,14 @@ func areaPaths(schema map[string]any) map[string]any {
|
||||
"get": map[string]any{"operationId": "get_" + id, "summary": "Read " + strings.Join(parts, "/"), "tags": []string{"settings"}, "security": sec,
|
||||
"parameters": params, "responses": ok200("The value.", s), "x-scope": scopeRead},
|
||||
"put": map[string]any{"operationId": "put_" + id, "summary": "Replace " + strings.Join(parts, "/"), "tags": []string{"settings"}, "security": sec,
|
||||
"parameters": withParams(ifMatch), "requestBody": jsonBody(node), "responses": ok200("Saved, not applied: the result has the new ETag and the difference.", map[string]any{"type": "object"}), "x-scope": scopeChange},
|
||||
"parameters": withParams(ifMatch), "requestBody": jsonBody(node), "responses": ok200("Saved, not applied: the result has the new ETag and the difference.", edited), "x-scope": scopeChange},
|
||||
"delete": map[string]any{"operationId": "delete_" + id, "summary": "Remove " + strings.Join(parts, "/") + " (its defaults apply again)", "tags": []string{"settings"}, "security": sec,
|
||||
"parameters": withParams(ifMatch), "responses": ok200("Saved, not applied.", map[string]any{"type": "object"}), "x-scope": scopeChange},
|
||||
"parameters": withParams(ifMatch), "responses": ok200("Saved, not applied.", edited), "x-scope": scopeChange},
|
||||
}
|
||||
if node["type"] == "object" {
|
||||
ops["patch"] = map[string]any{"operationId": "patch_" + id, "summary": "Merge into " + strings.Join(parts, "/"), "tags": []string{"settings"}, "security": sec,
|
||||
"parameters": withParams(ifMatch), "requestBody": jsonBody(map[string]any{"type": "object", "description": "A JSON merge patch (RFC 7386): objects merge, a value replaces, null removes a key, arrays are replaced whole."}),
|
||||
"responses": ok200("Saved, not applied.", map[string]any{"type": "object"}), "x-scope": scopeChange}
|
||||
"responses": ok200("Saved, not applied.", edited), "x-scope": scopeChange}
|
||||
}
|
||||
paths[path] = ops
|
||||
if len(parts) >= 3 {
|
||||
|
||||
Reference in New Issue
Block a user