338 lines
13 KiB
Go
338 lines
13 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"
|
|
|
|
// **A taken value may be laid under the file's own name** (issue 751). An entry of takes-config is a key, laid
|
|
// under that key, or {"key": <the config key>, "as": <the file's name for it>}: the key stays a config key,
|
|
// lower-case, and the file is written in its software's spelling, `atlassian-url` as ATLASSIAN_URL. Like every
|
|
// taken value it is laid only when a layer sets it, never from a default, so an unset one leaves the file's bytes
|
|
// and the code's own fallback (its environment) as they were; for that reason it is never `required`
|
|
// (ConfigProblems). The settings form has no such entry: it is the config form's alone.
|
|
|
|
// Fields of a renamed takes-config entry.
|
|
const (
|
|
takenKeyField = "key"
|
|
takenAsField = "as"
|
|
)
|
|
|
|
// takenEntry is one value a mergeable file takes: its key and the name the file has for it.
|
|
type takenEntry struct {
|
|
key, as string
|
|
renamed bool
|
|
}
|
|
|
|
// takenEntries is the entries of a takes-settings list in their order, a usable entry only: a string is a key
|
|
// under its own name, an object with exactly key and as a key under another (TakenSettingsProblems says the rest).
|
|
func takenEntries(raw any) []takenEntry {
|
|
list, _ := raw.([]any)
|
|
var out []takenEntry
|
|
for _, item := range list {
|
|
if e, ok := asTakenEntry(item); ok {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// asTakenEntry reads one item of a takes-settings list, and whether it is shaped as an entry at all.
|
|
func asTakenEntry(item any) (takenEntry, bool) {
|
|
switch v := item.(type) {
|
|
case string:
|
|
return takenEntry{key: v, as: v}, true
|
|
case map[string]any:
|
|
key, keyOK := v[takenKeyField].(string)
|
|
as, asOK := v[takenAsField].(string)
|
|
if !keyOK || !asOK || len(v) != 2 {
|
|
return takenEntry{}, false
|
|
}
|
|
return takenEntry{key: key, as: as, renamed: true}, true
|
|
}
|
|
return takenEntry{}, false
|
|
}
|
|
|
|
// takenKeyList is the keys a resource takes, in order: the config keys, never the file's names for them.
|
|
func takenKeyList(resource map[string]any) []string {
|
|
var keys []string
|
|
for _, e := range takenEntries(resource[TakesSettingsField]) {
|
|
keys = append(keys, e.key)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
// takenKeys is the keys a resource names in takes-settings, each with the name the file has for it, 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]string, bool) {
|
|
raw, said := resource[TakesSettingsField]
|
|
if !said {
|
|
return nil, false
|
|
}
|
|
keys := map[string]string{}
|
|
for _, e := range takenEntries(raw) {
|
|
keys[e.key] = e.as
|
|
}
|
|
return keys, true
|
|
}
|
|
|
|
// onlyTaken is the layers with every key the file does not name left out, and each key it takes under the
|
|
// file's name for it. The file's own defaults and what the mesh reads itself are not touched here: settle
|
|
// already keeps both out of a file.
|
|
//
|
|
// **A layer said before its module moved still reaches the file** (issue 751): such a layer names a renamed value
|
|
// by the file's own spelling (nodered's timeZone on one machine), and until K5 moves the layer and renames the key
|
|
// that spelling is taken as the value's, so moving the module changes no machine's file. Where one layer says
|
|
// both, the config key wins.
|
|
func onlyTaken(layers []Layer, taken map[string]string) []Layer {
|
|
oldSpelling := map[string]string{}
|
|
for key, as := range taken {
|
|
if as != key {
|
|
oldSpelling[as] = key
|
|
}
|
|
}
|
|
kept := make([]Layer, 0, len(layers))
|
|
for _, layer := range layers {
|
|
values := map[string]any{}
|
|
for key, value := range layer.Values {
|
|
if as, takes := taken[key]; takes {
|
|
values[as] = value
|
|
continue
|
|
}
|
|
if newKey, old := oldSpelling[key]; old {
|
|
if _, said := layer.Values[newKey]; !said {
|
|
values[key] = value
|
|
}
|
|
}
|
|
}
|
|
kept = append(kept, Layer{From: layer.From, Values: values, Default: layer.Default})
|
|
}
|
|
return kept
|
|
}
|
|
|
|
// takenNames is every key a layer may say for the values a resource takes: each config key and, for a renamed
|
|
// one, the file's own spelling a layer said before its module moved (onlyTaken).
|
|
func takenNames(resource map[string]any) []string {
|
|
var names []string
|
|
for _, e := range takenEntries(resource[TakesSettingsField]) {
|
|
names = append(names, e.key)
|
|
if e.renamed {
|
|
names = append(names, e.as)
|
|
}
|
|
}
|
|
return names
|
|
}
|
|
|
|
// 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 {
|
|
return takenSettingsProblems(m)
|
|
}
|
|
|
|
// WrittenSettingsProblems is ADR 0326 §3, judged only where a definition is written — the module
|
|
// check, `module add`, a build taken in, the builder and the merge gate's registration — and never
|
|
// where a stored one is read again (ParseManifest, a build's manifest at a commit, a module put back):
|
|
// a module that declares a setting names the keys of every mergeable file it owns (novox/hq issue
|
|
// 574, ADR 0164 §11 and §15). A build stored before the rule, such as betterbird's before its accounts
|
|
// file named its keys, must still compose and still be put back; only its next change is held to it.
|
|
//
|
|
// Said only when none of the module's mergeable files names its keys: when one does, §2 already
|
|
// names every file that does not, and a file is not reported twice.
|
|
func WrittenSettingsProblems(m Manifest) []string {
|
|
if len(m.Settings) == 0 || m.DeclaresConfig() {
|
|
// A module in the config form is held to this where it is read, in its own words (ConfigProblems).
|
|
return nil
|
|
}
|
|
var silent []string
|
|
for _, r := range m.Resources {
|
|
how, _ := r["merge"].(string)
|
|
if how == "" {
|
|
continue
|
|
}
|
|
if _, said := r[TakesSettingsField]; said {
|
|
return nil
|
|
}
|
|
silent = append(silent, fmt.Sprint(r["id"]))
|
|
}
|
|
var problems []string
|
|
for _, id := range silent {
|
|
problems = append(problems, fmt.Sprintf("%s declares settings, and its mergeable %s 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 §3, ADR 0164 §11)", m.Module, id, TakesSettingsField))
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// 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, landed := map[string]bool{}, map[string]bool{}
|
|
for _, item := range list {
|
|
if _, isObject := item.(map[string]any); isObject {
|
|
e, shaped := asTakenEntry(item)
|
|
switch {
|
|
case !m.DeclaresConfig():
|
|
problems = append(problems, fmt.Sprintf("%s's %s: %v in %s lays a value under another name, "+
|
|
"which only the config form says: write the key alone (novox/hq issue 751)",
|
|
m.Module, id, item, TakesSettingsField))
|
|
continue
|
|
case !shaped:
|
|
problems = append(problems, fmt.Sprintf("%s's %s: %v in %s is a key alone, or an object "+
|
|
"saying exactly %q and %q: the config key and the file's name for it (novox/hq issue 751)",
|
|
m.Module, id, item, TakesSettingsField, takenKeyField, takenAsField))
|
|
continue
|
|
case !settingRef.MatchString("${setting:" + e.key + "}"):
|
|
problems = append(problems, fmt.Sprintf("%s's %s: %q in %s is not a usable key: a config "+
|
|
"key is lower-case letters, digits, dots, dashes and underscores; the file's own spelling "+
|
|
"is its %q", m.Module, id, e.key, TakesSettingsField, takenAsField))
|
|
continue
|
|
case !usableKey(e.as):
|
|
problems = append(problems, fmt.Sprintf("%s's %s: %q, the name %s takes %q as, is not a "+
|
|
"usable name in a file", m.Module, id, e.as, TakesSettingsField, e.key))
|
|
continue
|
|
}
|
|
problems = append(problems, takenEntryProblems(m.Module, id, e, seen, landed, protected)...)
|
|
continue
|
|
}
|
|
key, isString := item.(string)
|
|
if !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))
|
|
continue
|
|
}
|
|
problems = append(problems, takenEntryProblems(m.Module, id, takenEntry{key: key, as: key}, seen,
|
|
landed, protected)...)
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
// takenEntryProblems is what is wrong with one usable entry of a file's takes-settings, against those before it:
|
|
// a key taken twice, or two values laid under one name in the file; a word the mesh reads itself, as the key or as
|
|
// the file's name; a name the file keeps as protected. seen and landed are the keys and the names so far, added to.
|
|
func takenEntryProblems(module, id string, e takenEntry, seen, landed, protected map[string]bool) []string {
|
|
var problems []string
|
|
switch {
|
|
case seen[e.key]:
|
|
problems = append(problems, fmt.Sprintf("%s's %s names %q twice in %s", module, id, e.key, TakesSettingsField))
|
|
case landed[e.as]:
|
|
problems = append(problems, fmt.Sprintf("%s's %s names %q twice in %s, as a key or as the name a key is "+
|
|
"laid under: one name in a file holds one value", module, id, e.as, TakesSettingsField))
|
|
case meshWords[e.key] || meshWords[e.as]:
|
|
word := e.key
|
|
if !meshWords[word] {
|
|
word = e.as
|
|
}
|
|
problems = append(problems, fmt.Sprintf("%s's %s names %q in %s, and the mesh reads %q itself "+
|
|
"(novox/hq ADR 0164 §14)", module, id, word, TakesSettingsField, word))
|
|
case protected[e.as]:
|
|
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", module, id, e.as, TakesSettingsField))
|
|
}
|
|
seen[e.key], landed[e.as] = true, true
|
|
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 := takenKeyList(r)
|
|
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, "; ") + ")"
|
|
}
|