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:
2026-11-01 20:09:20 +01:00
co-authored by Claude Sonnet 5.5
parent 42ceb78d2b
commit 64e08fb485
11 changed files with 1064 additions and 10 deletions
+29
View File
@@ -31,5 +31,34 @@
</section>
{{end}}
</div>
<section class="panel">
<h2>Settings areas</h2>
<p>The settings are a tree, and every part of it has its own path below <code>/api/v1/settings</code>:
<code>router/firewall</code>, <code>router/lans/lan</code> (one LAN, by its name), <code>router/dns/hosts</code>,
<code>system</code>, down to a single option like <code>router/wan/interface</code> (lists are replaced whole; their
elements have no paths). The OpenAPI document describes each with the type of its value.</p>
<ul>
<li><code>GET</code> reads the part (404 when it is not set and the defaults apply); the <code>ETag</code> header is the
settings' version.</li>
<li><code>PUT</code> replaces it, <code>PATCH</code> merges a JSON merge patch into it (objects only), <code>DELETE</code> removes
it. They need <code>If-Match</code> with the ETag, need a change token, and are refused (400, with the place) when a value
does not fit its option's type or a key is not an option.</li>
<li>Like every edit through the API they are saved, never applied; <code>GET /api/v1/settings/check</code> evaluates them.</li>
</ul>
<pre class="log">curl -sk -H "Authorization: Bearer $TOKEN" https://{{.Host}}/api/v1/settings/router/dns
curl -sk -X PUT -H "Authorization: Bearer $TOKEN" -H 'If-Match: "ETAG"' \
-d '{"domain":"home.example.org","dynamicLeases":true}' https://{{.Host}}/api/v1/settings/router/dns</pre>
</section>
{{range .Areas}}
<section class="panel">
<h2><code>{{.Path}}</code></h2>
<div class="scroll"><table class="opts">
<tr><th>Option</th><th>Type</th><th>Default</th><th>Description</th></tr>
{{range .Options}}<tr><td><code>{{.Name}}</code></td><td>{{.Type}}</td><td>{{with .Default}}<code>{{.}}</code>{{end}}</td><td>{{.Description}}</td></tr>{{end}}
</table></div>
<p class="hint"><code>&lt;name&gt;</code> is the key of an entry in a map, <code>*</code> an element of a list.</p>
</section>
{{end}}
{{end}}
{{end}}