ADR 0593 §7: the composer with its refusals leaves mesh-sdk, so mesh-sdk carries no composition rule. The catalogue functions only the node-engine's composition reaches (Resolve, Resolution's Compose, Declaration, DataItems, LeftOut, Rules and the rest, the settings, placeholder and template refusals, the rendering helpers) are copied with their tests into internal/compose/resolution; a method of a mesh-sdk shape becomes a function taking it first. The composition calls its own copies, and the captured catalogue its tests read lives here now, shared by mesh-compose's tests. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
217 lines
7.9 KiB
Go
217 lines
7.9 KiB
Go
package resolution
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"unicode"
|
|
|
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
const takenKeyField = "key"
|
|
|
|
const 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
|
|
}
|
|
|
|
// 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[mcat.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 []mcat.Layer, taken map[string]string) []mcat.Layer {
|
|
oldSpelling := map[string]string{}
|
|
for key, as := range taken {
|
|
if as != key {
|
|
oldSpelling[as] = key
|
|
}
|
|
}
|
|
kept := make([]mcat.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, mcat.Layer{From: layer.From, Values: values, Default: layer.Default})
|
|
}
|
|
return kept
|
|
}
|
|
|
|
// 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 mcat.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[mcat.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, mcat.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, mcat.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, mcat.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, mcat.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, mcat.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, mcat.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, mcat.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, mcat.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, mcat.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, mcat.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, mcat.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, mcat.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
|
|
}
|