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:}` 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:}`: 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, "credential": 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, } // 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. var aboutAnAmount = map[string]bool{ "max": true, "min": true, "num": true, "count": true, "show": true, "hide": true, "enable": true, "disable": true, "use": true, "allow": 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 { if pair := words[n-2] + "-" + words[n-1]; 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:}`. 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 }