A module left out for an unknown key inside an entry lost its whole manifest, so every consumer of what it provides was refused and its data stopped being copied. Read past only the unknown key, keep its backup lines, and say in the condition what stops.
225 lines
8.8 KiB
Go
225 lines
8.8 KiB
Go
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.
|
|
// Only said: the layer is recognised by Layer.Default, never by this name, which a node may also have.
|
|
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 the words that, as what a key's name is about, 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 = map[string]bool{
|
|
"domain": true, "host": true, "hostname": true, "servername": true, "fqdn": true, "zone": true,
|
|
"realm": true, "tenant": true, "site": true, "timezone": true,
|
|
"issuer": true, "url": true, "uri": true, "webhook": true, "origin": true, "dsn": true,
|
|
"ip": true, "ipv4": true, "ipv6": true,
|
|
"email": true, "mail": true, "phone": true, "address": true,
|
|
"identity": true, "login": true, "user": true, "username": true, "account": true, "owner": true,
|
|
"uid": true, "gid": true, "puid": true, "pgid": true,
|
|
"password": true, "pass": true, "passwd": true, "passphrase": true, "secret": true, "token": true,
|
|
"key": true, "apikey": true, "bearer": true, "cert": true, "certificate": true, "credential": true,
|
|
"nameserver": true, "gateway": true, "subnet": true, "sender": true, "recipient": true, "contact": true,
|
|
}
|
|
|
|
// operatorsCompounds are names of two words that are the operator's though neither word alone says so
|
|
// at the end of a key: a client's identifier, and a name the world knows a site or server by.
|
|
var operatorsCompounds = map[string]bool{
|
|
"client-id": true, "site-name": true, "server-name": true, "public-name": true, "smtp-relay": true,
|
|
"host-name": true, "user-name": true, "domain-name": true, "dns-server": true,
|
|
// Whom a rule lets in or keeps out: a list of addresses or networks.
|
|
"allow-from": true, "deny-from": true,
|
|
}
|
|
|
|
// aboutAnAmount are first words that make a key about how many or whether, never about whom:
|
|
// `max-tokens` is a number, `show-hostname` a switch. Not `allow` or `use`: `allow-from` and
|
|
// `use-host` name whom.
|
|
var aboutAnAmount = map[string]bool{
|
|
"max": true, "min": true, "num": true, "count": true, "show": true, "hide": true, "enable": true,
|
|
"disable": true,
|
|
}
|
|
|
|
// operatorsWord is what in a key's name says its value is the operator's, or "". A key is about its
|
|
// last word — `url-timeout` is a timeout, `user-agent` an agent, `mail-domain` a domain — or its last two
|
|
// as one of operatorsCompounds. A plural is read as its singular.
|
|
func operatorsWord(key string) string {
|
|
words := strings.FieldsFunc(key, func(r rune) bool { return r == '-' || r == '_' || r == '.' })
|
|
if len(words) == 0 || (len(words) > 1 && aboutAnAmount[words[0]]) {
|
|
return ""
|
|
}
|
|
for i, w := range words {
|
|
if !operatorsOwn[w] && strings.HasSuffix(w, "s") && operatorsOwn[strings.TrimSuffix(w, "s")] {
|
|
words[i] = strings.TrimSuffix(w, "s")
|
|
}
|
|
}
|
|
if n := len(words); n > 1 {
|
|
for _, last := range []string{words[n-1], strings.TrimSuffix(words[n-1], "s")} {
|
|
if pair := words[n-2] + "-" + last; operatorsCompounds[pair] {
|
|
return pair
|
|
}
|
|
}
|
|
}
|
|
if last := words[len(words)-1]; operatorsOwn[last] {
|
|
return last
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
if word := operatorsWord(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)
|
|
}
|
|
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, Default: true}, 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
|
|
// FromDefault is whether the value is the module's default, whatever From reads.
|
|
FromDefault bool
|
|
// 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, FromDefault: true,
|
|
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, s.FromDefault = v, layer.From, layer.Default
|
|
}
|
|
}
|
|
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
|
|
}
|