From 29be985c235ca692e2e1937884f603cc707edbab Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 30 Sep 2026 22:34:43 +0200 Subject: [PATCH] =?UTF-8?q?A=20served=20value=20or=20a=20contribution's=20?= =?UTF-8?q?may=20be=20the=20operator's:=20${setting:=E2=80=A6}=20fills=20t?= =?UTF-8?q?here=20too,=20refused=20by=20name=20when=20unset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue 173's rule needs it: a mail provider serves its domain and an identity provider its issuer, and neither is the definition's to state. Declared as ${setting:}, filled from the layers after the overrides; a key so asked for is not stray. --- internal/catalogue/setting_into.go | 29 +++++++++++++++++++++++++---- internal/catalogue/settings.go | 28 +++++++++++++++++++++++++++- internal/catalogue/settings_test.go | 21 +++++++++++++++++++++ 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/internal/catalogue/setting_into.go b/internal/catalogue/setting_into.go index f63199f..751f3b1 100644 --- a/internal/catalogue/setting_into.go +++ b/internal/catalogue/setting_into.go @@ -91,19 +91,40 @@ func orNoSettings(layers []Layer) string { return "; set today: " + strings.Join(keys, ", ") } -// settingKeysUsedBy is every key a module's files ask for, so a setting that lands in one is not -// called stray. +// settingKeysUsedBy is every key a module's files, contributions and served facts ask for, so a +// setting that lands in one is not called stray. func settingKeysUsedBy(m Manifest) map[string]bool { used := map[string]bool{} + note := func(s string) { + for _, k := range settingsUsed(s) { + used[k] = true + } + } for _, r := range m.Resources { if fmt.Sprint(r["type"]) != "file" { continue } if content, ok := r["content"].(string); ok { - for _, k := range settingsUsed(content) { - used[k] = true + note(content) + } + } + inValues := func(values map[string]any) { + for _, v := range values { + if s, ok := v.(string); ok { + note(s) } } } + for _, values := range m.Contributes { + inValues(values) + } + for _, locals := range m.ContributesMany { + for _, values := range locals { + inValues(values) + } + } + for _, values := range m.Serves { + inValues(values) + } return used } diff --git a/internal/catalogue/settings.go b/internal/catalogue/settings.go index 7d84980..6ea87ce 100644 --- a/internal/catalogue/settings.go +++ b/internal/catalogue/settings.go @@ -99,6 +99,11 @@ func Settle(base map[string]any, layers []Layer) (map[string]any, error) { // overridden lays settings over a map whose keys are its contract: a contribution, a served fact. // Only the keys the map already declares are touched; the rest of a layer is somebody else's // business (a file's, another destination's) and is left to reach it there. +// +// A declared value may itself be the operator's, `${setting:}` (ADR 0155): a mail provider +// serves its domain, an identity provider its issuer, and neither is the definition's to state. +// Filled from the layers after the overrides, and refused by name when nothing sets it — a literal +// placeholder handed to a consumer is a service configured against a string nobody meant. func overridden(base map[string]any, layers []Layer, what string) (map[string]any, error) { kept := make([]Layer, 0, len(layers)) for _, layer := range layers { @@ -110,7 +115,28 @@ func overridden(base map[string]any, layers []Layer, what string) (map[string]an } kept = append(kept, Layer{From: layer.From, Values: values}) } - return settle(base, kept, nil, what) + merged, err := settle(base, kept, nil, what) + if err != nil { + return nil, err + } + for key, value := range merged { + s, ok := value.(string) + if !ok { + continue + } + for _, asked := range settingsUsed(s) { + v, set := settingValue(layers, asked) + if !set { + return nil, fmt.Errorf( + "%s says ${setting:%s} for %q, and nothing sets %q — an operator's value is the "+ + "assignment's, never the definition's (novox/hq ADR 0112)%s", + what, asked, key, asked, orNoSettings(layers)) + } + s = strings.ReplaceAll(s, "${setting:"+asked+"}", plainly(v)) + } + merged[key] = s + } + return merged, nil } // settle lays the layers over a module's own values, in order. diff --git a/internal/catalogue/settings_test.go b/internal/catalogue/settings_test.go index 2b2a89f..b2cc392 100644 --- a/internal/catalogue/settings_test.go +++ b/internal/catalogue/settings_test.go @@ -213,3 +213,24 @@ func TestContentThatIsNotJSONIsRefusedWhereSomebodyIsLooking(t *testing.T) { t.Fatal("a module claiming to merge as JSON shipped something else and was accepted") } } + +func TestAServedValueMayBeTheOperators(t *testing.T) { + // A mail provider serves its domain and an identity provider its issuer; neither is the + // definition's to state (ADR 0155). Filled from the layers, refused by name when unset. + served, err := Settle(map[string]any{"port": 587, "domain": "${setting:domain}"}, + []Layer{{From: "the mesh", Values: map[string]any{"domain": "example.tld"}}}) + if err != nil { + t.Fatal(err) + } + if served["domain"] != "example.tld" { + t.Errorf("the operator's value did not fill the served key: %v", served) + } + _, err = Settle(map[string]any{"domain": "${setting:domain}"}, nil) + if err == nil || !strings.Contains(err.Error(), `"domain"`) { + t.Errorf("a served value nothing sets must be refused by name; got %v", err) + } + m := Manifest{Module: "mail", Serves: map[string]map[string]any{"smtp": {"domain": "${setting:domain}"}}} + if unused := UnusedSettings(m, []Layer{{From: "the mesh", Values: map[string]any{"domain": "x"}}}); len(unused) != 0 { + t.Errorf("a setting a served fact asks for is not stray: %v", unused) + } +}