mesh/merge-gate pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without …
mesh/repo-check fail: its merge-check.sh failed: --- FAIL: TestModuleCheckPassesTheCatalogue (0.89s); and TestEveryCatalogueManifestParses, TestNoCatalogue…
mesh/delivery superseded: a newer head of the same pull request
Every key of a module's layers merged into every mergeable file it owns:
the mesh's own words, a key meant for a ${setting:} in another file, a
key for a second configuration file. A module could not own two
configurable files. A file now names its keys in takes-settings and
takes only those; a key no file names is stray where it is set; a
module that declares a setting names the keys of every mergeable file,
and one that declares none keeps today's form (ADR 0164 §15).
188 lines
6.8 KiB
Go
188 lines
6.8 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
// A mergeable file names the setting keys it takes (novox/hq ADR 0326, issue 168).
|
|
//
|
|
// Before this, every key of a module's layers merged into every one of its mergeable files: the
|
|
// mesh's own words, a key meant for a `${setting:<key>}` in another file, a key for a second
|
|
// configuration file. So a module could not own two configurable files, and every receiver had to
|
|
// ignore what it did not know. ADR 0164 §11 decided that a setting names the files that read it; this
|
|
// is that relation, stated from the file's side, so the operator's own values stay declared nowhere
|
|
// (ADR 0262 §2).
|
|
|
|
// TakesSettingsField is the key a mergeable file carries to name the top-level setting keys it takes.
|
|
const TakesSettingsField = "takes-settings"
|
|
|
|
// takenKeys is the keys a resource names in takes-settings, and whether it names any list at all. An
|
|
// empty list is a list: that file takes no setting.
|
|
func takenKeys(resource map[string]any) (map[string]bool, bool) {
|
|
raw, said := resource[TakesSettingsField]
|
|
if !said {
|
|
return nil, false
|
|
}
|
|
keys := map[string]bool{}
|
|
for _, k := range stringsOf(raw) {
|
|
keys[k] = true
|
|
}
|
|
return keys, true
|
|
}
|
|
|
|
// onlyTaken is the layers with every key the file does not name left out. The file's own defaults and
|
|
// what the mesh reads itself are not touched here: settle already keeps both out of a file.
|
|
func onlyTaken(layers []Layer, taken map[string]bool) []Layer {
|
|
kept := make([]Layer, 0, len(layers))
|
|
for _, layer := range layers {
|
|
values := map[string]any{}
|
|
for key, value := range layer.Values {
|
|
if taken[key] {
|
|
values[key] = value
|
|
}
|
|
}
|
|
kept = append(kept, Layer{From: layer.From, Values: values, Default: layer.Default})
|
|
}
|
|
return kept
|
|
}
|
|
|
|
// namesItsKeys says whether a module's mergeable files name the keys they take (ADR 0326 §2): true
|
|
// only when it has one and every one carries takes-settings. A module with a silent mergeable file
|
|
// keeps today's form (§3), where every key reaches every mergeable file.
|
|
func namesItsKeys(m Manifest) bool {
|
|
merging := 0
|
|
for _, r := range m.Resources {
|
|
if how, _ := r["merge"].(string); how == "" {
|
|
continue
|
|
}
|
|
merging++
|
|
if _, said := r[TakesSettingsField]; !said {
|
|
return false
|
|
}
|
|
}
|
|
return merging > 0
|
|
}
|
|
|
|
// TakenSettingsProblems is every way a definition's takes-settings is wrong (ADR 0326 §2 and §4), in
|
|
// its own words: on a file that does not merge, not a list of keys, a key named twice, one the mesh
|
|
// reads itself, one the file keeps for itself, or one mergeable file naming its keys beside another
|
|
// that names none.
|
|
func TakenSettingsProblems(m Manifest) []string {
|
|
problems := takenSettingsProblems(m)
|
|
// **A module that declares a setting names the keys of every mergeable file** (ADR 0326 §3, ADR
|
|
// 0164 §11 and §15): only a module that declares none keeps today's form. Said where a definition
|
|
// is checked and registered, never where a stored one is composed: a definition stored before
|
|
// this record is brought into the form by its next change, not left out of a machine meanwhile.
|
|
if len(m.Settings) > 0 {
|
|
for _, r := range m.Resources {
|
|
how, _ := r["merge"].(string)
|
|
if _, said := r[TakesSettingsField]; how != "" && !said {
|
|
problems = append(problems, fmt.Sprintf("%s declares settings, and its mergeable %v names no %s: "+
|
|
"a module that declares a setting names the keys each mergeable file takes, an empty list "+
|
|
"for none (novox/hq ADR 0326, ADR 0164 §11)", m.Module, r["id"], TakesSettingsField))
|
|
}
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// takenSettingsProblems is what TakenSettingsProblems says that a stored definition is judged by too.
|
|
func takenSettingsProblems(m Manifest) []string {
|
|
var problems []string
|
|
var naming, silent []string
|
|
for _, r := range m.Resources {
|
|
id := fmt.Sprint(r["id"])
|
|
how, _ := r["merge"].(string)
|
|
raw, said := r[TakesSettingsField]
|
|
if how == "" {
|
|
if said {
|
|
problems = append(problems, fmt.Sprintf("%s's %s says %s and does not merge: only a mergeable "+
|
|
"file takes settings by key (novox/hq ADR 0326)", m.Module, id, TakesSettingsField))
|
|
}
|
|
continue
|
|
}
|
|
if !said {
|
|
silent = append(silent, id)
|
|
continue
|
|
}
|
|
naming = append(naming, id)
|
|
list, isList := raw.([]any)
|
|
if !isList {
|
|
problems = append(problems, fmt.Sprintf("%s's %s: %s is a list of setting keys, and this is %T",
|
|
m.Module, id, TakesSettingsField, raw))
|
|
continue
|
|
}
|
|
protected := map[string]bool{}
|
|
for _, k := range stringsOf(r["protected"]) {
|
|
protected[k] = true
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, item := range list {
|
|
key, isString := item.(string)
|
|
switch {
|
|
case !isString || !usableKey(key):
|
|
problems = append(problems, fmt.Sprintf("%s's %s: %v in %s is not a usable setting key",
|
|
m.Module, id, item, TakesSettingsField))
|
|
case seen[key]:
|
|
problems = append(problems, fmt.Sprintf("%s's %s names %q twice in %s",
|
|
m.Module, id, key, TakesSettingsField))
|
|
case meshWords[key]:
|
|
problems = append(problems, fmt.Sprintf("%s's %s names %q in %s, and the mesh reads %q itself "+
|
|
"(novox/hq ADR 0164 §14)", m.Module, id, key, TakesSettingsField, key))
|
|
case protected[key]:
|
|
problems = append(problems, fmt.Sprintf("%s's %s names %q in %s and keeps it as protected: a "+
|
|
"key is the file's or the setting's, never both", m.Module, id, key, TakesSettingsField))
|
|
}
|
|
if isString {
|
|
seen[key] = true
|
|
}
|
|
}
|
|
}
|
|
if len(naming) > 0 {
|
|
for _, id := range silent {
|
|
problems = append(problems, fmt.Sprintf("%s's %s merges and names no %s while %s does: once one "+
|
|
"mergeable file of a module names the keys it takes, every one does, an empty list for none "+
|
|
"(novox/hq ADR 0326)", m.Module, id, TakesSettingsField, strings.Join(naming, ", ")))
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// usableKey is a key a file can be said to take: not empty, and no space, line end or control
|
|
// character. Not the lower-case rule of a `${setting:<key>}`: a file's own keys are its software's
|
|
// spelling, such as `timeZone`.
|
|
func usableKey(key string) bool {
|
|
if key == "" {
|
|
return false
|
|
}
|
|
for _, r := range key {
|
|
if unicode.IsSpace(r) || unicode.IsControl(r) || !unicode.IsPrint(r) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// takenSaid is what a stray key's message adds for a module whose files name their keys: which keys
|
|
// each file takes, so the person sees where the key would have had to be named.
|
|
func takenSaid(m Manifest) string {
|
|
if !namesItsKeys(m) {
|
|
return ""
|
|
}
|
|
var files []string
|
|
for _, r := range m.Resources {
|
|
if how, _ := r["merge"].(string); how == "" {
|
|
continue
|
|
}
|
|
keys := stringsOf(r[TakesSettingsField])
|
|
if len(keys) == 0 {
|
|
files = append(files, fmt.Sprintf("%v takes none", r["id"]))
|
|
continue
|
|
}
|
|
files = append(files, fmt.Sprintf("%v takes %s", r["id"], strings.Join(keys, ", ")))
|
|
}
|
|
return " (" + strings.Join(files, "; ") + ")"
|
|
}
|