mesh/delivery ready: it delivers once merged
mesh/merge-gate out of date: judged against a trunk it does not name, the trunk is now f8daec01d3f0; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against a trunk it does not name, the trunk is now f8daec01d3f0; judged again on a recheck or when it is merged
177 lines
6.2 KiB
Go
177 lines
6.2 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 {
|
|
// Not yet here: refusing a module that declares a setting and owns a mergeable file naming no keys
|
|
// (ADR 0326 §3). betterbird is one, and the catalogue cannot name its keys before this controller
|
|
// runs, so that refusal follows once the catalogue does (novox/hq issue 574).
|
|
return takenSettingsProblems(m)
|
|
}
|
|
|
|
// takenSettingsProblems is every rule of ADR 0326 §2 and §4, judged where a definition is checked and
|
|
// where a stored one is composed.
|
|
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, "; ") + ")"
|
|
}
|