Add typed settings areas to the REST API
/api/v1/settings/router/firewall, .../router/lans/<name>, .../system, down to a single option: GET reads, PUT replaces, PATCH merges, DELETE removes, with the settings' ETag/If-Match and the same limits as the whole-document edit (editSettings is now shared). Values are checked against a JSON Schema made from the option documentation (schema.go reads the types, which nixosOptionsDoc describes in words); a wrong type, a value out of range or an unknown key is refused with its place. The OpenAPI document has the area paths with their schemas; the API reference page lists every area's options. Tests: unit tests for the type reader, the validator and the paths; the real option file and a router's own settings validate (ROUTER_OPTIONS_JSON, LIVE_SETTINGS_JSON); a subtest in apply. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
@@ -54,6 +54,7 @@ func (a *app) registerAPI(mux *http.ServeMux) {
|
||||
for _, rt := range apiRoutes {
|
||||
mux.HandleFunc(rt.method+" "+rt.path, a.apiHandler(rt))
|
||||
}
|
||||
a.registerAreas(mux)
|
||||
mux.HandleFunc("GET /api/v1/options", a.apiOptions)
|
||||
mux.HandleFunc("GET /api/openapi.json", a.apiSpec)
|
||||
}
|
||||
@@ -172,10 +173,10 @@ func (a *app) apiSpec(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
_ = enc.Encode(openAPISpec(a.sys.installedRelease().Version))
|
||||
_ = enc.Encode(openAPISpec(a.sys.installedRelease().Version, a.settingsSchema()))
|
||||
}
|
||||
|
||||
func openAPISpec(version string) map[string]any {
|
||||
func openAPISpec(version string, schema map[string]any) map[string]any {
|
||||
errResp := func(d string) map[string]any {
|
||||
return map[string]any{"description": d, "content": map[string]any{"application/json": map[string]any{"schema": map[string]any{"$ref": "#/components/schemas/Error"}}}}
|
||||
}
|
||||
@@ -235,13 +236,19 @@ func openAPISpec(version string) map[string]any {
|
||||
"security": []any{map[string]any{"bearer": []string{}}},
|
||||
"responses": map[string]any{"200": map[string]any{"description": "The options (JSON)", "content": map[string]any{"application/json": map[string]any{"schema": map[string]any{}}}}, "404": errResp("Not available.")},
|
||||
}}
|
||||
if schema != nil {
|
||||
for p, ops := range areaPaths(schema) {
|
||||
paths[p] = ops
|
||||
}
|
||||
}
|
||||
return map[string]any{
|
||||
"openapi": "3.1.0",
|
||||
"info": map[string]any{
|
||||
"title": "nixos-router",
|
||||
"version": version,
|
||||
"description": "The router's management API, equal to its MCP server: read its state and logs, edit the saved settings. " +
|
||||
"Changes are saved, never applied: a person applies them in the web UI. Tokens are made on the API access page (scope read or change).",
|
||||
"Changes are saved, never applied: a person applies them in the web UI. Tokens are made on the API access page (scope read or change). " +
|
||||
"The settings are also a tree of areas: /api/v1/settings/router/firewall, /router/lans/{name}, /system, ... (any depth) can be read, replaced (PUT), merged into (PATCH) or removed (DELETE); the documented ones go three levels deep.",
|
||||
},
|
||||
"servers": []any{map[string]any{"url": "/"}},
|
||||
"paths": paths,
|
||||
|
||||
Reference in New Issue
Block a user