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:
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -69,5 +70,48 @@ func apiReference(host string) []apiDoc {
|
||||
func (a *app) apiReference(w http.ResponseWriter, r *http.Request, sess session) {
|
||||
a.render(w, "apiref", page{Title: "API reference", CSRF: sess.csrf, LoggedIn: true, Data: map[string]any{
|
||||
"Base": "https://" + r.Host, "Ops": apiReference(r.Host), "HasOptions": a.optionsFile != "",
|
||||
"Areas": settingsAreas(a.optionsFile), "Host": r.Host,
|
||||
}})
|
||||
}
|
||||
|
||||
type areaOption struct{ Name, Type, Default, Description string }
|
||||
|
||||
type areaDoc struct {
|
||||
Path string
|
||||
Options []areaOption
|
||||
}
|
||||
|
||||
// settingsAreas lists the areas below router/ with their options (from the
|
||||
// option documentation): what a person can read and write there.
|
||||
func settingsAreas(optionsFile string) []areaDoc {
|
||||
if optionsFile == "" {
|
||||
return nil
|
||||
}
|
||||
entries, err := loadOptions(optionsFile)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
byArea := map[string][]areaOption{}
|
||||
for _, e := range entries {
|
||||
if len(e.Loc) < 3 || e.Loc[0] != "router" {
|
||||
continue
|
||||
}
|
||||
desc := strings.TrimSpace(e.Description)
|
||||
if i := strings.Index(desc, "\n\n"); i > 0 {
|
||||
desc = desc[:i]
|
||||
}
|
||||
desc = strings.Join(strings.Fields(desc), " ")
|
||||
if len(desc) > 300 {
|
||||
desc = desc[:300] + "…"
|
||||
}
|
||||
name := strings.Join(e.Loc[2:], ".")
|
||||
byArea[e.Loc[1]] = append(byArea[e.Loc[1]], areaOption{name, e.Type, literal(e.Default), desc})
|
||||
}
|
||||
var areas []areaDoc
|
||||
for k, opts := range byArea {
|
||||
sort.Slice(opts, func(i, j int) bool { return opts[i].Name < opts[j].Name })
|
||||
areas = append(areas, areaDoc{Path: "/api/v1/settings/router/" + k, Options: opts})
|
||||
}
|
||||
sort.Slice(areas, func(i, j int) bool { return areas[i].Path < areas[j].Path })
|
||||
return areas
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user