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:
@@ -497,25 +497,37 @@ func (a *app) updateSettings(args json.RawMessage) (any, error) {
|
||||
if _, ok := in.Patch.(map[string]any); !ok {
|
||||
return nil, errors.New("`patch` must be an object")
|
||||
}
|
||||
return a.editSettings(in.Etag, func(next Settings) (Settings, error) {
|
||||
patched, _ := mergePatch(map[string]any(next), in.Patch).(map[string]any)
|
||||
return Settings(patched), nil
|
||||
})
|
||||
}
|
||||
|
||||
// editSettings saves what edit makes of the settings, if etag is the current
|
||||
// version of them (so nobody's edit is lost) and the result stays within what
|
||||
// the API may change. The settings stay unapplied.
|
||||
func (a *app) editSettings(wantEtag string, edit func(next Settings) (Settings, error)) (any, error) {
|
||||
a.settingsMu.Lock()
|
||||
defer a.settingsMu.Unlock()
|
||||
data, err := os.ReadFile(a.sys.settingsPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.Etag != etag(data) {
|
||||
if wantEtag != etag(data) {
|
||||
return nil, errStale
|
||||
}
|
||||
old, err := parseSettings(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
next, err := parseSettings(data) // a copy to patch
|
||||
next, err := parseSettings(data) // a copy to edit
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
next, err = edit(next)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patched, _ := mergePatch(map[string]any(next), in.Patch).(map[string]any)
|
||||
next = Settings(patched)
|
||||
if err := a.sys.checkMCPSettings(old, next); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -525,5 +537,5 @@ func (a *app) updateSettings(args json.RawMessage) (any, error) {
|
||||
after, _ := os.ReadFile(a.sys.settingsPath())
|
||||
return map[string]any{"saved": true, "applied": false, "etag": etag(after),
|
||||
"changes": diffLines(pretty(data), pretty(after)),
|
||||
"next": "Run check_settings; a person applies the changes in the web UI."}, nil
|
||||
"next": "Run check_settings (GET /api/v1/settings/check); a person applies the changes in the web UI."}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user