Fill a preference's ${setting:} from its manifest default (hq ADR 0262)
Without a default, a running module could never gain a setting: the file asking for it failed to compose until set, and the key was refused as stray until a file asked for it. Defaults sit under the mesh's and the node's settings, never merge into a JSON file, are refused for the operator's own values, and settings shows each value's source.
This commit is contained in:
@@ -502,13 +502,38 @@ func settingsCommand(ctx context.Context, args []string) error {
|
||||
}
|
||||
if !has {
|
||||
fmt.Printf("%s on %s: no layer — the module's definition says\n", positionals[0], where)
|
||||
return nil
|
||||
} else {
|
||||
shown, err := json.MarshalIndent(values, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(shown))
|
||||
}
|
||||
shown, err := json.MarshalIndent(values, "", " ")
|
||||
// Every value the module gives a default or a layer sets, and where it came from (novox/hq
|
||||
// ADR 0262): the default, the mesh's layer, or this node's. Said after the layer, which stays
|
||||
// the first thing printed because a caller reads it before replacing it (ADR 0217).
|
||||
known, err := inv.Catalogue(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(shown))
|
||||
m, ok := known[positionals[0]]
|
||||
if !ok || len(m.Settings) == 0 {
|
||||
return nil
|
||||
}
|
||||
var layers []catalogue.Layer
|
||||
if *node != "" {
|
||||
if mesh, has, err := inv.Layer(ctx, "", positionals[0]); err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
layers = append(layers, catalogue.Layer{From: catalogue.MeshWideLayer, Values: mesh})
|
||||
}
|
||||
if has {
|
||||
layers = append(layers, catalogue.Layer{From: *node, Values: values})
|
||||
}
|
||||
} else if has {
|
||||
layers = append(layers, catalogue.Layer{From: catalogue.MeshWideLayer, Values: values})
|
||||
}
|
||||
fmt.Print(describeEffective(positionals[0], where, catalogue.Effective(m, layers)))
|
||||
return nil
|
||||
|
||||
case "clear":
|
||||
@@ -526,6 +551,23 @@ func settingsCommand(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// describeEffective says each setting's value on a machine or the whole mesh, where it came from, and
|
||||
// the module's default when a layer overrides it.
|
||||
func describeEffective(module, where string, values []catalogue.SettingSource) string {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "%s on %s, every value and where it comes from:\n", module, where)
|
||||
for _, v := range values {
|
||||
value, _ := json.Marshal(v.Value)
|
||||
fmt.Fprintf(&b, " %s = %s (%s", v.Key, value, v.From)
|
||||
if v.HasDefault && v.From != catalogue.DefaultLayer {
|
||||
d, _ := json.Marshal(v.Default)
|
||||
fmt.Fprintf(&b, "; the default is %s", d)
|
||||
}
|
||||
b.WriteString(")\n")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// nodeFlag is ` --node <node>` for a machine's layer, nothing for the whole mesh's.
|
||||
func nodeFlag(node string) string {
|
||||
if node == "" {
|
||||
|
||||
@@ -1425,7 +1425,7 @@ func servedOnNode(ctx context.Context, inv *inventory.Inventory, node string,
|
||||
}
|
||||
serves := catalogue.ServedOn(m, provision, ports)
|
||||
if len(serves) > 0 {
|
||||
serves, err = catalogue.Settle(serves, layers)
|
||||
serves, err = catalogue.Settle(serves, catalogue.WithDefaults(m, layers))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
|
||||
// `settings` says each value and where it came from, and the default a layer overrides (novox/hq ADR 0262).
|
||||
func TestSettingsSayWhereEachValueComesFrom(t *testing.T) {
|
||||
got := describeEffective("dunst", "laptop", []catalogue.SettingSource{
|
||||
{Key: "font-size", Value: float64(13), From: "laptop", Default: float64(10), HasDefault: true},
|
||||
{Key: "width", Value: float64(250), From: catalogue.DefaultLayer, Default: float64(250), HasDefault: true},
|
||||
})
|
||||
want := "dunst on laptop, every value and where it comes from:\n" +
|
||||
" font-size = 13 (laptop; the default is 10)\n" +
|
||||
" width = 250 (default)\n"
|
||||
if got != want {
|
||||
t.Fatalf("said\n%s\nwant\n%s", got, want)
|
||||
}
|
||||
}
|
||||
@@ -238,7 +238,7 @@ func (r Resolution) derivedFor(provision, as, consumer, local string, settings S
|
||||
"different things and nothing would compare them (novox/hq ADR 0202)",
|
||||
consumer, local, provision, m.Module, orNothing(sortedAnyKeys(names)))
|
||||
}
|
||||
settled, err := Settle(names, settings[m.Module])
|
||||
settled, err := Settle(names, WithDefaults(m, settings[m.Module]))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s serving %s: %w", m.Module, provision, err)
|
||||
}
|
||||
|
||||
@@ -907,7 +907,7 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
||||
// **An operator's value, from the assignment** (novox/hq ADR 0112, ADR 0155): what a
|
||||
// definition may not carry because it is true of one installation only. Filled from
|
||||
// the same layers a mergeable file takes, and refused when no layer set it.
|
||||
if err := settingInto(copied, with.Settings[m.Module], m.Module); err != nil {
|
||||
if err := settingInto(copied, WithDefaults(m, with.Settings[m.Module]), m.Module); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// **Placed before anything reads a path.** A pathless directory receives the path
|
||||
@@ -1495,7 +1495,7 @@ func (r Resolution) composed(m Manifest, to string, raw map[string]any, layers [
|
||||
// Overridden, not merged: a setting changes a key the contribution declares and adds none.
|
||||
// The provider reads the contribution as a contract, and a setting made for one of this
|
||||
// module's files is no part of it (novox/hq 04-ISSUES/173).
|
||||
values, err := overridden(raw, layers, what)
|
||||
values, err := overridden(raw, WithDefaults(m, layers), what)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", what, err)
|
||||
}
|
||||
@@ -1926,7 +1926,7 @@ func (r Resolution) servedOnThisMachine(provision string, with Rendering) (map[s
|
||||
// one, so keep looking rather than concluding from the first.
|
||||
continue
|
||||
}
|
||||
settled, err := Settle(serves, with.Settings[m.Module])
|
||||
settled, err := Settle(serves, WithDefaults(m, with.Settings[m.Module]))
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("%s serving %s: %w", m.Module, provision, err)
|
||||
}
|
||||
|
||||
@@ -458,6 +458,13 @@ type Manifest struct {
|
||||
// (novox/hq ADR 0201). Not history — that is an event — and never a secret, sealed or not.
|
||||
State []StateDeclaration `json:"state,omitempty"`
|
||||
|
||||
// Settings are the defaults this module gives its settings (novox/hq ADR 0262): each key a file,
|
||||
// a contribution or a served fact asks for as `${setting:<key>}`, its default, and why that
|
||||
// default. Only a preference has one — a font size, a width, a number of workers — and a value
|
||||
// that is inherently the operator's (a domain, an identity, a secret) is declared nowhere here and
|
||||
// stays refused by name until a layer sets it. The mesh's layer, then the node's, override it.
|
||||
Settings map[string]SettingDeclaration `json:"settings,omitempty"`
|
||||
|
||||
// Data is every kind of data this module keeps — its own, by directory, and what it keeps for
|
||||
// its consumers, by provision — each with a class the mesh protects and watches it by (novox/hq
|
||||
// ADR 0233). One list: the backup holder's lines, the bindings that do not move, what an
|
||||
@@ -1541,6 +1548,8 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
problems = append(problems, EventProblems(m)...)
|
||||
// And what it may call its state, and whose it may read (state.go, novox/hq ADR 0201).
|
||||
problems = append(problems, StateProblems(m)...)
|
||||
// And the defaults it gives its settings (setting_defaults.go, novox/hq ADR 0262).
|
||||
problems = append(problems, SettingProblems(m)...)
|
||||
wellFormed := true
|
||||
for _, c := range m.Claims {
|
||||
if !name.MatchString(c.Name) {
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A preference has a default; the operator's own value has none (novox/hq ADR 0262, extending ADR
|
||||
// 0112 and taking the default half of ADR 0164).
|
||||
//
|
||||
// `${setting:<key>}` was refused whenever no layer set the key, and `settings set` refused a key no
|
||||
// file asked for yet. Together they meant a running module could never gain a setting: the file that
|
||||
// asks for it fails to compose until somebody sets it, and nobody can set it until the file asks.
|
||||
// A definition may now give a key a default, with why, when the value is a **preference** — a font
|
||||
// size, a width, a number of workers: something true of the software that a machine may tune. A
|
||||
// value that is inherently the operator's — a domain, a public name, an identity, a secret — gets no
|
||||
// default and stays refused by name until a layer sets it, which is what ADR 0112 and ADR 0155 were
|
||||
// written for.
|
||||
//
|
||||
// The default is the lowest layer. The mesh's layer, then the node's, override it. It fills only
|
||||
// `${setting:<key>}`: a mergeable file's content already is its defaults, and laying a default over
|
||||
// it would reach every mergeable file of the module (novox/hq issue 168).
|
||||
|
||||
// SettingDeclaration is one key's default, as the definition gives it.
|
||||
type SettingDeclaration struct {
|
||||
// Kind says what sort of value this is. `preference` is the only kind with a default; it is
|
||||
// stated rather than assumed so a reviewer sees the claim being made.
|
||||
Kind string `json:"kind"`
|
||||
// Default is the value when no layer sets the key: a string, a number or a boolean.
|
||||
Default any `json:"default"`
|
||||
// Why this default: one sentence for whoever wonders whether to change it.
|
||||
Why string `json:"why"`
|
||||
}
|
||||
|
||||
// KindPreference is the kind of a setting that may have a default.
|
||||
const KindPreference = "preference"
|
||||
|
||||
// DefaultLayer is what the layer of a module's own defaults is called where a value's source is said.
|
||||
const DefaultLayer = "default"
|
||||
|
||||
// meshWords are the settings keys the mesh reads itself; a module declares none of them.
|
||||
var meshWords = map[string]bool{
|
||||
PortsSetting: true, ExposeSetting: true, ReachSetting: true, EndpointsSetting: true,
|
||||
PlacesSetting: true, AccessesSetting: true, NetworksSetting: true,
|
||||
}
|
||||
|
||||
// operatorsOwn are words that, in a key's name, say its value is the operator's and never a
|
||||
// preference: a default for one would be the literal ADR 0112 removed from definitions.
|
||||
var operatorsOwn = []string{
|
||||
"domain", "host", "issuer", "url", "email", "mail", "address", "identity", "login", "user",
|
||||
"account", "password", "secret", "token", "credential",
|
||||
}
|
||||
|
||||
// SettingProblems is every way a definition's setting defaults are wrong, in its own words.
|
||||
func SettingProblems(m Manifest) []string {
|
||||
if len(m.Settings) == 0 {
|
||||
return nil
|
||||
}
|
||||
used := settingKeysUsedBy(m)
|
||||
var problems []string
|
||||
for _, key := range sortedSettingKeys(m.Settings) {
|
||||
d := m.Settings[key]
|
||||
say := func(format string, args ...any) {
|
||||
problems = append(problems, fmt.Sprintf("%s's setting %q: ", m.Module, key)+fmt.Sprintf(format, args...))
|
||||
}
|
||||
if !settingRef.MatchString("${setting:" + key + "}") {
|
||||
say("not a usable key: lower-case letters, digits, dots, dashes and underscores")
|
||||
continue
|
||||
}
|
||||
if meshWords[key] {
|
||||
say("the mesh reads %q itself, and a module gives it no default", key)
|
||||
continue
|
||||
}
|
||||
if d.Kind != KindPreference {
|
||||
say("kind is %q, and only a %q has a default (novox/hq ADR 0262)", d.Kind, KindPreference)
|
||||
}
|
||||
for _, word := range operatorsOwn {
|
||||
if strings.Contains(key, word) {
|
||||
say("a key naming %q is the operator's value, and has no default — it is the "+
|
||||
"assignment's, never the definition's (novox/hq ADR 0112, ADR 0262)", word)
|
||||
break
|
||||
}
|
||||
}
|
||||
switch v := d.Default.(type) {
|
||||
case string:
|
||||
if strings.TrimSpace(v) == "" {
|
||||
say("an empty default is no default; give the value, or declare nothing")
|
||||
}
|
||||
case float64, bool:
|
||||
case nil:
|
||||
say("no default: a key without one is the operator's, and is not declared here")
|
||||
default:
|
||||
say("a default is a string, a number or a boolean, and this is %T", d.Default)
|
||||
}
|
||||
if strings.TrimSpace(d.Why) == "" {
|
||||
say("no why: say in one sentence why this default")
|
||||
}
|
||||
if !used[key] {
|
||||
say("nothing asks for ${setting:%s}, so the default reaches nothing", key)
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// Defaults is the layer a module's own defaults make, or nothing when it gives none.
|
||||
func Defaults(m Manifest) (Layer, bool) {
|
||||
if len(m.Settings) == 0 {
|
||||
return Layer{}, false
|
||||
}
|
||||
values := map[string]any{}
|
||||
for key, d := range m.Settings {
|
||||
if d.Default != nil {
|
||||
values[key] = d.Default
|
||||
}
|
||||
}
|
||||
return Layer{From: DefaultLayer, Values: values}, len(values) > 0
|
||||
}
|
||||
|
||||
// WithDefaults is a module's layers with its defaults under them, for filling `${setting:<key>}`.
|
||||
func WithDefaults(m Manifest, layers []Layer) []Layer {
|
||||
d, has := Defaults(m)
|
||||
if !has {
|
||||
return layers
|
||||
}
|
||||
return append([]Layer{d}, layers...)
|
||||
}
|
||||
|
||||
// SettingSource is one key's effective value and the layer it came from.
|
||||
type SettingSource struct {
|
||||
Key string
|
||||
Value any
|
||||
// From is DefaultLayer, MeshWideLayer or the node's name.
|
||||
From string
|
||||
// Default is the module's default, when it gives one.
|
||||
Default any
|
||||
HasDefault bool
|
||||
}
|
||||
|
||||
// Effective is every key a module gives a default or a layer sets, with its value and where it came
|
||||
// from: the default, then the mesh's layer, then the node's — later wins.
|
||||
func Effective(m Manifest, layers []Layer) []SettingSource {
|
||||
byKey := map[string]*SettingSource{}
|
||||
for key, d := range m.Settings {
|
||||
byKey[key] = &SettingSource{Key: key, Value: d.Default, From: DefaultLayer, Default: d.Default, HasDefault: true}
|
||||
}
|
||||
for _, layer := range layers {
|
||||
for key, v := range layer.Values {
|
||||
s, ok := byKey[key]
|
||||
if !ok {
|
||||
s = &SettingSource{Key: key}
|
||||
byKey[key] = s
|
||||
}
|
||||
s.Value, s.From = v, layer.From
|
||||
}
|
||||
}
|
||||
out := make([]SettingSource, 0, len(byKey))
|
||||
for _, s := range byKey {
|
||||
out = append(out, *s)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Key < out[j].Key })
|
||||
return out
|
||||
}
|
||||
|
||||
func sortedSettingKeys(in map[string]SettingDeclaration) []string {
|
||||
keys := make([]string, 0, len(in))
|
||||
for k := range in {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A preference has a default in the definition; the mesh's layer, then the node's, override it; a key
|
||||
// with a default is not stray; and a value that is the operator's still has none (novox/hq ADR 0262).
|
||||
|
||||
func notifier() Manifest {
|
||||
return Manifest{Module: "notifier",
|
||||
Settings: map[string]SettingDeclaration{
|
||||
"font-size": {Kind: KindPreference, Default: float64(10), Why: "readable at a scale of one"},
|
||||
"width": {Kind: KindPreference, Default: float64(250), Why: "fits a title of forty characters"},
|
||||
},
|
||||
Resources: []map[string]any{{"id": "configuration", "type": "file", "path": "/x/notifierrc",
|
||||
"content": "font = Inter ${setting:font-size}\nwidth = ${setting:width}\n"}},
|
||||
}
|
||||
}
|
||||
|
||||
func parsed(t *testing.T, m Manifest) error {
|
||||
t.Helper()
|
||||
raw, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = ParseManifest(raw)
|
||||
return err
|
||||
}
|
||||
|
||||
func TestAnUnsetPreferenceTakesItsDefault(t *testing.T) {
|
||||
m := notifier()
|
||||
for _, layers := range [][]Layer{nil, {{From: MeshWideLayer, Values: map[string]any{}}}} {
|
||||
file := map[string]any{}
|
||||
for k, v := range m.Resources[0] {
|
||||
file[k] = v
|
||||
}
|
||||
if err := settingInto(file, WithDefaults(m, layers), m.Module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if file["content"] != "font = Inter 10\nwidth = 250\n" {
|
||||
t.Fatalf("filled as %q", file["content"])
|
||||
}
|
||||
}
|
||||
if err := JudgeSettings(m, nil, false); err != nil {
|
||||
t.Fatalf("a module whose every key has a default does not compose with no layer: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheNodeOverTheMeshOverTheDefault(t *testing.T) {
|
||||
m := notifier()
|
||||
layers := []Layer{
|
||||
{From: MeshWideLayer, Values: map[string]any{"width": float64(300)}},
|
||||
{From: "laptop", Values: map[string]any{"font-size": float64(13), "width": float64(340)}},
|
||||
}
|
||||
file := map[string]any{"type": "file", "content": m.Resources[0]["content"]}
|
||||
if err := settingInto(file, WithDefaults(m, layers), m.Module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if file["content"] != "font = Inter 13\nwidth = 340\n" {
|
||||
t.Fatalf("filled as %q", file["content"])
|
||||
}
|
||||
file = map[string]any{"type": "file", "content": m.Resources[0]["content"]}
|
||||
if err := settingInto(file, WithDefaults(m, layers[:1]), m.Module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if file["content"] != "font = Inter 10\nwidth = 300\n" {
|
||||
t.Fatalf("the mesh's layer over the default filled as %q", file["content"])
|
||||
}
|
||||
}
|
||||
|
||||
// Composed for a machine, the way a push writes it: the default reaches the file, and the node's
|
||||
// layer overrides it.
|
||||
func TestAComposedMachineGetsTheDefaultAndTheNodesValue(t *testing.T) {
|
||||
r := anAdoptedAnchor()
|
||||
r.Modules = append(r.Modules, notifier())
|
||||
with := anchorRendering(false)
|
||||
composed, err := r.Compose(with)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if why, left := composed.LeftOut["notifier"]; left {
|
||||
t.Fatalf("the notifier was left out: %s", why)
|
||||
}
|
||||
if c := byID(composed.Resources)["notifier.configuration"]["content"]; c != "font = Inter 10\nwidth = 250\n" {
|
||||
t.Fatalf("composed with no layer as %q", c)
|
||||
}
|
||||
with.Settings["notifier"] = []Layer{{From: "anchor", Values: map[string]any{"font-size": float64(13)}}}
|
||||
if composed, err = r.Compose(with); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c := byID(composed.Resources)["notifier.configuration"]["content"]; c != "font = Inter 13\nwidth = 250\n" {
|
||||
t.Fatalf("composed with the node's font size as %q", c)
|
||||
}
|
||||
}
|
||||
|
||||
// Setting a key the module gives a default is not refused as reaching nothing: it overrides the
|
||||
// default, which is how a running module gains a setting with no gap between.
|
||||
func TestAKeyWithADefaultIsNotStray(t *testing.T) {
|
||||
m := notifier()
|
||||
m.Resources[0]["content"] = "font = Inter ${setting:font-size}\nwidth = ${setting:width}\n"
|
||||
stray := UnusedSettings(m, []Layer{{From: "laptop", Values: map[string]any{"font-size": float64(13), "colour": "red"}}})
|
||||
joined := strings.Join(stray, "; ")
|
||||
if strings.Contains(joined, `"font-size"`) || !strings.Contains(joined, `"colour"`) {
|
||||
t.Fatalf("stray: %s", joined)
|
||||
}
|
||||
}
|
||||
|
||||
// A default fills ${setting:…} only: it never becomes a key of a mergeable file (issue 168).
|
||||
func TestADefaultIsNotMergedIntoAJSONFile(t *testing.T) {
|
||||
m := notifier()
|
||||
m.Resources = append(m.Resources, map[string]any{"id": "other", "type": "file", "path": "/x/other.json",
|
||||
"merge": MergeJSON, "content": `{"keep": 1}`})
|
||||
out, err := ApplySettings(m.Resources[1], WithDefaults(m, nil))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(out["content"].(string), "font-size") {
|
||||
t.Fatalf("a default reached a mergeable file: %s", out["content"])
|
||||
}
|
||||
}
|
||||
|
||||
// A value that is the operator's has no default: no layer setting it is refused by name, as before.
|
||||
func TestAnOperatorsValueWithoutADefaultIsStillRefused(t *testing.T) {
|
||||
m := notifier()
|
||||
m.Resources[0]["content"] = "font = Inter ${setting:font-size}\nwidth = ${setting:width}\nfrom = ${setting:domain}\n"
|
||||
err := JudgeSettings(m, nil, false)
|
||||
if err == nil || !strings.Contains(err.Error(), "${setting:domain}") {
|
||||
t.Fatalf("judged %v", err)
|
||||
}
|
||||
if strings.Contains(err.Error(), "font-size") {
|
||||
t.Fatalf("a default was named as set today: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheParserTakesAPreferenceAndRefusesTheRest(t *testing.T) {
|
||||
if err := parsed(t, notifier()); err != nil {
|
||||
t.Fatalf("a preference with a default was refused: %v", err)
|
||||
}
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
key string
|
||||
d SettingDeclaration
|
||||
refuse string
|
||||
}{
|
||||
{"no kind", "font-size", SettingDeclaration{Default: float64(10), Why: "x"}, `only a "preference" has a default`},
|
||||
{"another kind", "font-size", SettingDeclaration{Kind: "operator", Default: float64(10), Why: "x"}, `only a "preference"`},
|
||||
{"no default", "font-size", SettingDeclaration{Kind: KindPreference, Why: "x"}, "no default"},
|
||||
{"an empty default", "font-size", SettingDeclaration{Kind: KindPreference, Default: " ", Why: "x"}, "an empty default"},
|
||||
{"an object", "font-size", SettingDeclaration{Kind: KindPreference, Default: map[string]any{"a": 1.0}, Why: "x"}, "a string, a number or a boolean"},
|
||||
{"no why", "font-size", SettingDeclaration{Kind: KindPreference, Default: float64(10)}, "no why"},
|
||||
{"the operator's", "mail-domain", SettingDeclaration{Kind: KindPreference, Default: "example.tld", Why: "x"}, "is the operator's value"},
|
||||
{"a secret", "api-token", SettingDeclaration{Kind: KindPreference, Default: "x", Why: "x"}, "is the operator's value"},
|
||||
{"the mesh's word", PortsSetting, SettingDeclaration{Kind: KindPreference, Default: float64(1), Why: "x"}, "the mesh reads"},
|
||||
{"read by nothing", "height", SettingDeclaration{Kind: KindPreference, Default: float64(300), Why: "x"}, "reaches nothing"},
|
||||
} {
|
||||
m := notifier()
|
||||
m.Resources[0]["content"] = m.Resources[0]["content"].(string) + "x = ${setting:" + c.key + "}\n"
|
||||
if c.name == "read by nothing" {
|
||||
m.Resources[0]["content"] = "font = Inter ${setting:font-size}\nwidth = ${setting:width}\n"
|
||||
}
|
||||
m.Settings[c.key] = c.d
|
||||
err := parsed(t, m)
|
||||
if err == nil || !strings.Contains(err.Error(), c.refuse) {
|
||||
t.Errorf("%s: parsed %v, want %q", c.name, err, c.refuse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEveryValueSaysWhereItCameFrom(t *testing.T) {
|
||||
m := notifier()
|
||||
m.Resources[0]["content"] = m.Resources[0]["content"].(string) + "x = ${setting:position}\n"
|
||||
got := Effective(m, []Layer{
|
||||
{From: MeshWideLayer, Values: map[string]any{"width": float64(300), "position": "top-right"}},
|
||||
{From: "laptop", Values: map[string]any{"font-size": float64(13)}},
|
||||
})
|
||||
want := map[string]string{"font-size": "laptop", "position": MeshWideLayer, "width": MeshWideLayer}
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("effective: %+v", got)
|
||||
}
|
||||
for _, s := range got {
|
||||
if s.From != want[s.Key] {
|
||||
t.Errorf("%s from %q, want %q", s.Key, s.From, want[s.Key])
|
||||
}
|
||||
}
|
||||
if got[0].Key != "font-size" || got[0].Value != float64(13) || got[0].Default != float64(10) {
|
||||
t.Fatalf("font-size: %+v", got[0])
|
||||
}
|
||||
only := Effective(m, nil)
|
||||
if only[0].From != DefaultLayer || only[0].Value != float64(10) {
|
||||
t.Fatalf("with no layer: %+v", only)
|
||||
}
|
||||
}
|
||||
@@ -40,8 +40,9 @@ func settingsUsed(content string) []string {
|
||||
|
||||
// settingInto fills a file's ${setting:…} placeholders from the layers over a module.
|
||||
//
|
||||
// The last layer setting a key wins, which is the node's over the mesh's — the same order settle
|
||||
// applies to a mergeable file. A value that is not a string is written the way a program would read
|
||||
// The last layer setting a key wins, which is the node's over the mesh's over the module's own
|
||||
// default (novox/hq ADR 0262) — the same order settle applies to a mergeable file. The caller lays
|
||||
// the defaults under the layers with WithDefaults. A value that is not a string is written the way a program would read
|
||||
// it (a number without a trailing .000000, a boolean as true/false).
|
||||
func settingInto(resource map[string]any, layers []Layer, module string) error {
|
||||
if fmt.Sprint(resource["type"]) != "file" {
|
||||
@@ -56,7 +57,8 @@ func settingInto(resource map[string]any, layers []Layer, module string) error {
|
||||
if !set {
|
||||
return fmt.Errorf(
|
||||
"%s has a file that says ${setting:%s}, and nothing sets %q for it — an operator's "+
|
||||
"value is the assignment's, never the definition's (novox/hq ADR 0112): "+
|
||||
"value is the assignment's, never the definition's (novox/hq ADR 0112), and only a "+
|
||||
"preference has a default in the definition (ADR 0262): "+
|
||||
"`settings set %s <file>` with {%q: …}%s",
|
||||
module, key, key, module, key, orNoSettings(layers))
|
||||
}
|
||||
@@ -80,6 +82,9 @@ func settingValue(layers []Layer, key string) (any, bool) {
|
||||
func orNoSettings(layers []Layer) string {
|
||||
var keys []string
|
||||
for _, l := range layers {
|
||||
if l.From == DefaultLayer {
|
||||
continue
|
||||
}
|
||||
for k := range l.Values {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
@@ -151,6 +151,12 @@ func settle(base map[string]any, layers []Layer, protected map[string]bool, what
|
||||
map[string]any, error) {
|
||||
merged := deepCopy(base)
|
||||
for _, layer := range layers {
|
||||
if layer.From == DefaultLayer {
|
||||
// A module's own defaults fill ${setting:<key>} only (novox/hq ADR 0262). A mergeable
|
||||
// file's content already is its defaults, and a contribution or served fact declares its
|
||||
// own; laid on here, a default would reach every mergeable file of its module (issue 168).
|
||||
continue
|
||||
}
|
||||
for key, value := range layer.Values {
|
||||
if key == PortsSetting {
|
||||
// Where the machine puts a port is the mesh's to apply, not a value for a file or
|
||||
@@ -224,6 +230,11 @@ func UnusedSettings(m Manifest, layers []Layer) []string {
|
||||
}
|
||||
|
||||
lands := settingKeysUsedBy(m)
|
||||
// A key the module gives a default reaches what asks for it (novox/hq ADR 0262): setting it
|
||||
// overrides the default, which is how a running module gains a setting without a gap between.
|
||||
for key := range m.Settings {
|
||||
lands[key] = true
|
||||
}
|
||||
for _, values := range m.Contributes {
|
||||
for key := range values {
|
||||
lands[key] = true
|
||||
@@ -420,7 +431,7 @@ func JudgeSettings(m Manifest, layers []Layer, adopted bool) error {
|
||||
for k, v := range settled {
|
||||
copied[k] = v
|
||||
}
|
||||
if err := settingInto(copied, layers, m.Module); err != nil {
|
||||
if err := settingInto(copied, WithDefaults(m, layers), m.Module); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +249,9 @@ var ControllerVerbs = []Verb{
|
||||
}, nil, "adopted"),
|
||||
Replaces: []string{"mesh-controller token issue", "wg set"}},
|
||||
{Name: "settings", Description: "Read or set what an assignment is configured with: a module's settings for the whole " +
|
||||
"mesh, or for one machine. Without values or clear, answers the layer as it stands — read it before setting it; " +
|
||||
"mesh, or for one machine. Without values or clear, answers the layer as it stands — read it before setting it — " +
|
||||
"then every value the module gives a default or a layer sets, with where it comes from: the module's default, " +
|
||||
"the mesh, or the machine (novox/hq ADR 0262); " +
|
||||
"with history, the layers it replaced. Setting replaces that layer whole and answers each key it adds (+), " +
|
||||
"changes (~) and removes (-); a set that would remove a key is refused unless replace says it is meant " +
|
||||
"(novox/hq ADR 0217). Takes effect at the next push. With clear, removes the layer and the module is back to " +
|
||||
|
||||
Reference in New Issue
Block a user