Bring F3 up to its release branch, with both sides' store tests (part 7 of 14)
mesh/merge-gate head 2e9b732914fe merged into trunk fcdecb436117 — fail: head 2e9b732914fe does not merge cleanly into trunk fcdecb436117: in conflict cmd/…
mesh/repo-check head 2e9b732914fe merged into trunk fcdecb436117 — fail: head 2e9b732914fe does not merge cleanly into trunk fcdecb436117: in conflict cmd/…
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head 2e9b732914fe merged into trunk fcdecb436117 — fail: head 2e9b732914fe does not merge cleanly into trunk fcdecb436117: in conflict cmd/…
mesh/repo-check head 2e9b732914fe merged into trunk fcdecb436117 — fail: head 2e9b732914fe does not merge cleanly into trunk fcdecb436117: in conflict cmd/…
mesh/delivery superseded: a newer head of the same pull request
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,469 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A module declares its config (novox/hq ADR 0348 §1, §4, §5, §6, §8, §11; design 57 package K1, issue 660).
|
||||
//
|
||||
// **One concept, two spellings for a while.** "Config" replaces "settings": the manifest says `config` for
|
||||
// `settings`, `takes-config` for `takes-settings` and `${config:<key>}` for `${setting:<key>}`. The old names stay
|
||||
// accepted as aliases until every module has moved (ADR 0348 §1), and a module uses one form or the other, never
|
||||
// both. The config form is read into the very fields the settings form fills — Settings, a mergeable file's
|
||||
// takes-settings and `${setting:<key>}` — so composition has one path and `${config:<key>}` fills exactly as
|
||||
// `${setting:<key>}` does: a module rewritten from settings to config composes byte-identically. It is written
|
||||
// back in the form it was read in, so what the mesh holds is what its author wrote.
|
||||
//
|
||||
// **A moved module is held to the whole declaration.** Under `config` every value has a type (number, string or
|
||||
// boolean; an object or a list only when trusted), a description including its unit, its validation (enum,
|
||||
// minimum and maximum, pattern), whether it is trusted, optionally the predefined key it falls back to, and either
|
||||
// a default or `required`; it lands in a file or is read at runtime (`runtime: true`); every mergeable file names
|
||||
// the keys it takes; a `reload-on` or `restart-on` names a file the module declares. A module not yet moved keeps
|
||||
// today's form and today's rules (SettingProblems, TakenSettingsProblems) until the catalogue's list of such
|
||||
// modules is empty (ADR 0348 §11).
|
||||
//
|
||||
// **A required value nothing sets** is the operator's own: it composes as an operator's setting nobody gave does
|
||||
// today, refused by name at composition (UnsetSettingError). Who reports it once mesh-config answers values is the
|
||||
// provisioner's (ADR 0349), not this file's.
|
||||
|
||||
// ConfigField is the manifest key a module declares its config values under.
|
||||
const ConfigField = "config"
|
||||
|
||||
// TakesConfigField is the key a mergeable file names the config values it takes by.
|
||||
const TakesConfigField = "takes-config"
|
||||
|
||||
// SettingsField is the old name of ConfigField, read as its alias.
|
||||
const SettingsField = "settings"
|
||||
|
||||
// configRef is how a file asks for a config value: ${config:<key>}, the same keys as settingRef.
|
||||
var configRef = regexp.MustCompile(`\$\{config:([a-z0-9][a-z0-9_.-]*)\}`)
|
||||
|
||||
// takesConfigKey and takesSettingsKey are the two spellings as a key of a JSON object. A string value is never
|
||||
// followed by a colon, and a quote inside a string is escaped, so these match only keys.
|
||||
var (
|
||||
takesConfigKey = regexp.MustCompile(`"` + TakesConfigField + `"(\s*):`)
|
||||
takesSettingsKey = regexp.MustCompile(`"` + TakesSettingsField + `"(\s*):`)
|
||||
)
|
||||
|
||||
// The types a config value may have (ADR 0348 §4).
|
||||
const (
|
||||
ConfigNumber = "number"
|
||||
ConfigString = "string"
|
||||
ConfigBoolean = "boolean"
|
||||
ConfigObject = "object"
|
||||
ConfigList = "list"
|
||||
)
|
||||
|
||||
// accountFacts are what the mesh places for a module's account, never config values (ADR 0348 §11).
|
||||
var accountFacts = map[string]bool{"puid": true, "pgid": true}
|
||||
|
||||
// PredefinedKey is one key of the catalogue's list of predefined keys, as far as a module's value falling back
|
||||
// to it is judged here: its type, and whether it is trusted (ADR 0348 §7, §8).
|
||||
type PredefinedKey struct {
|
||||
Type string
|
||||
Trusted bool
|
||||
}
|
||||
|
||||
// predefinedKeys is the list of predefined keys, nil until it is present. The list (`predefined.json` at the
|
||||
// catalogue's root) arrives with design 57's package K2 (issue 661); until then a value naming one is refused
|
||||
// saying the list is not yet present.
|
||||
var predefinedKeys map[string]PredefinedKey
|
||||
|
||||
// configForms is which of the two forms a manifest was written in, by the words of each it uses.
|
||||
type configForms struct {
|
||||
config, settings []string
|
||||
}
|
||||
|
||||
// DeclaresConfig is whether the manifest is written in the config form: `config`, `takes-config` or
|
||||
// `${config:<key>}` (ADR 0348 §1, §11).
|
||||
func (m Manifest) DeclaresConfig() bool { return len(m.forms.config) > 0 }
|
||||
|
||||
// toSettingsForm is raw with the config form's placeholders and takes-config read as the settings form's, and
|
||||
// which words of each form it used. `config` itself is a top-level key, renamed where the keys are read
|
||||
// (UnmarshalJSON).
|
||||
func toSettingsForm(raw []byte) ([]byte, configForms) {
|
||||
var f configForms
|
||||
if configRef.Match(raw) {
|
||||
f.config = append(f.config, "${config:…}")
|
||||
}
|
||||
if takesConfigKey.Match(raw) {
|
||||
f.config = append(f.config, TakesConfigField)
|
||||
}
|
||||
if settingRef.Match(raw) {
|
||||
f.settings = append(f.settings, "${setting:…}")
|
||||
}
|
||||
if takesSettingsKey.Match(raw) {
|
||||
f.settings = append(f.settings, TakesSettingsField)
|
||||
}
|
||||
if len(f.config) == 0 {
|
||||
return raw, f
|
||||
}
|
||||
raw = configRef.ReplaceAllFunc(raw, func(ref []byte) []byte {
|
||||
return []byte("${setting:" + string(configRef.FindSubmatch(ref)[1]) + "}")
|
||||
})
|
||||
raw = takesConfigKey.ReplaceAllFunc(raw, func(key []byte) []byte {
|
||||
return []byte(`"` + TakesSettingsField + `"` + string(takesConfigKey.FindSubmatch(key)[1]) + ":")
|
||||
})
|
||||
return raw, f
|
||||
}
|
||||
|
||||
// toConfigForm is a manifest written in the settings form's words, as JSON, in the config form's: what
|
||||
// toSettingsForm and UnmarshalJSON read, written back.
|
||||
func toConfigForm(raw []byte) ([]byte, error) {
|
||||
var keys map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &keys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if settings, has := keys[SettingsField]; has {
|
||||
keys[ConfigField] = settings
|
||||
delete(keys, SettingsField)
|
||||
var err error
|
||||
if raw, err = json.Marshal(keys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
raw = settingRef.ReplaceAllFunc(raw, func(ref []byte) []byte {
|
||||
return []byte("${config:" + string(settingRef.FindSubmatch(ref)[1]) + "}")
|
||||
})
|
||||
raw = takesSettingsKey.ReplaceAllFunc(raw, func(key []byte) []byte {
|
||||
return []byte(`"` + TakesConfigField + `"` + string(takesSettingsKey.FindSubmatch(key)[1]) + ":")
|
||||
})
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
// inConfigWords is a refusal said of a module in the config form in its words: `${config:<key>}` and
|
||||
// takes-config, where the rules shared with the settings form say `${setting:<key>}` and takes-settings.
|
||||
func inConfigWords(problem string) string {
|
||||
problem = settingRef.ReplaceAllStringFunc(problem, func(ref string) string {
|
||||
return "${config:" + settingRef.FindStringSubmatch(ref)[1] + "}"
|
||||
})
|
||||
return strings.ReplaceAll(problem, TakesSettingsField, TakesConfigField)
|
||||
}
|
||||
|
||||
// MixedFormProblems is a manifest using both forms, said with the words of each (ADR 0348 §1).
|
||||
func MixedFormProblems(m Manifest) []string {
|
||||
if len(m.forms.config) == 0 || len(m.forms.settings) == 0 {
|
||||
return nil
|
||||
}
|
||||
return []string{fmt.Sprintf("%s mixes the config form (%s) with the settings form (%s): a module uses one form "+
|
||||
"or the other, never both; write %s, %s and ${config:<key>} throughout (novox/hq ADR 0348 §1)",
|
||||
m.Module, strings.Join(m.forms.config, ", "), strings.Join(m.forms.settings, ", "),
|
||||
ConfigField, TakesConfigField)}
|
||||
}
|
||||
|
||||
// configDeclaration is the config form's declaration as written: nothing of the settings form's, and nothing
|
||||
// unsaid.
|
||||
type configDeclaration struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Enum []any `json:"enum,omitempty"`
|
||||
Minimum *float64 `json:"minimum,omitempty"`
|
||||
Maximum *float64 `json:"maximum,omitempty"`
|
||||
Pattern string `json:"pattern,omitempty"`
|
||||
Trusted bool `json:"trusted,omitempty"`
|
||||
Predefined string `json:"predefined,omitempty"`
|
||||
Default any `json:"default,omitempty"`
|
||||
Required bool `json:"required,omitempty"`
|
||||
Runtime bool `json:"runtime,omitempty"`
|
||||
}
|
||||
|
||||
// MarshalJSON writes a settings declaration exactly as before, and a config declaration without the settings
|
||||
// form's kind and why, and without a default it does not have.
|
||||
func (d SettingDeclaration) MarshalJSON() ([]byte, error) {
|
||||
type plain SettingDeclaration
|
||||
if d.Kind != "" || d.Why != "" {
|
||||
return json.Marshal(plain(d))
|
||||
}
|
||||
return json.Marshal(configDeclaration{Type: d.Type, Description: d.Description, Enum: d.Enum,
|
||||
Minimum: d.Minimum, Maximum: d.Maximum, Pattern: d.Pattern, Trusted: d.Trusted, Predefined: d.Predefined,
|
||||
Default: d.Default, Required: d.Required, Runtime: d.Runtime})
|
||||
}
|
||||
|
||||
// configShaped is whether a declaration says anything of the config form's.
|
||||
func configShaped(d SettingDeclaration) bool {
|
||||
return d.Type != "" || d.Description != "" || len(d.Enum) > 0 || d.Minimum != nil || d.Maximum != nil ||
|
||||
d.Pattern != "" || d.Trusted || d.Predefined != "" || d.Required || d.Runtime
|
||||
}
|
||||
|
||||
// TypedSettingProblems is a declaration under `settings` that says what only `config` says: a value with a type,
|
||||
// a description and its validation is declared in the config form (ADR 0348 §1, §4).
|
||||
func TypedSettingProblems(m Manifest) []string {
|
||||
var problems []string
|
||||
for _, key := range sortedSettingKeys(m.Settings) {
|
||||
if configShaped(m.Settings[key]) {
|
||||
problems = append(problems, fmt.Sprintf("%s's setting %q says its type, description or validation "+
|
||||
"under %s: a value declared with those is declared under config, with ${config:<key>} and "+
|
||||
"takes-config, never under %s (novox/hq ADR 0348 §1, §4)", m.Module, key, SettingsField, SettingsField))
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// ConfigProblems is every way a module in the config form declares its config wrongly, each said by name with
|
||||
// what to do (ADR 0348 How it is checked, design 57 package K1).
|
||||
func ConfigProblems(m Manifest) []string {
|
||||
if !m.DeclaresConfig() {
|
||||
return nil
|
||||
}
|
||||
var problems []string
|
||||
placed := configPlaced(m)
|
||||
for _, key := range sortedSettingKeys(m.Settings) {
|
||||
d := m.Settings[key]
|
||||
say := func(format string, args ...any) {
|
||||
problems = append(problems, fmt.Sprintf("%s's config value %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] || accountFacts[key] {
|
||||
say("%q is placement, which the mesh decides, never a config value (novox/hq ADR 0348 §11)", key)
|
||||
continue
|
||||
}
|
||||
if d.Kind != "" || d.Why != "" {
|
||||
say("kind and why are the settings form's; under config a value says its type and its description " +
|
||||
"(novox/hq ADR 0348 §1, §4)")
|
||||
}
|
||||
switch d.Type {
|
||||
case ConfigNumber, ConfigString, ConfigBoolean:
|
||||
case ConfigObject, ConfigList:
|
||||
if !d.Trusted {
|
||||
say("is %s and not trusted: an object or a list decides more than one value can say, so it is "+
|
||||
"trusted, \"trusted\": true, or split into numbers, strings and booleans (novox/hq ADR 0348 §4)", d.Type)
|
||||
}
|
||||
case "":
|
||||
say("no type: say \"type\": number, string or boolean, or object or list when trusted (novox/hq ADR 0348 §4)")
|
||||
default:
|
||||
say("type %q is none of number, string, boolean, object and list (novox/hq ADR 0348 §4)", d.Type)
|
||||
}
|
||||
if strings.TrimSpace(d.Description) == "" {
|
||||
say("no description: say in a sentence what the value is, including its unit (novox/hq ADR 0348 §4)")
|
||||
}
|
||||
switch {
|
||||
case d.Required && d.Default != nil:
|
||||
say("both a default and required: a required value is the operator's own and has no default; give " +
|
||||
"one or the other (novox/hq ADR 0348 §4)")
|
||||
case !d.Required && d.Default == nil:
|
||||
say("neither a default nor required: give a default, or say \"required\": true for a value that is " +
|
||||
"the operator's own (novox/hq ADR 0348 §4)")
|
||||
}
|
||||
for _, wrong := range validationProblems(d) {
|
||||
say("%s", wrong)
|
||||
}
|
||||
if word := operatorsWord(key); word != "" && d.Default != nil {
|
||||
say("a key naming %q is the operator's value: declare it \"required\": true, with no default "+
|
||||
"(novox/hq ADR 0348 §4, ADR 0112)", word)
|
||||
}
|
||||
if d.Predefined != "" {
|
||||
p, listed := predefinedKeys[d.Predefined]
|
||||
switch {
|
||||
case predefinedKeys == nil:
|
||||
say("falls back to the predefined key %q, and the list of predefined keys (the catalogue's "+
|
||||
"predefined.json, design 57 package K2) is not yet present: no value can fall back to one "+
|
||||
"until it is; leave \"predefined\" out for now (novox/hq ADR 0348 §7)", d.Predefined)
|
||||
case !listed:
|
||||
say("falls back to %q, which is not on the list of predefined keys: name a key on it, or leave "+
|
||||
"\"predefined\" out (novox/hq ADR 0348 §7)", d.Predefined)
|
||||
default:
|
||||
if p.Type != d.Type {
|
||||
say("falls back to the predefined key %q, which is a %s, and this value is a %s: a value falls "+
|
||||
"back only to a key of its own type (novox/hq ADR 0348 §4)", d.Predefined, p.Type, d.Type)
|
||||
}
|
||||
if p.Trusted && !d.Trusted {
|
||||
say("falls back to the trusted predefined key %q and is not trusted itself: a value that falls "+
|
||||
"back to a trusted key is trusted, \"trusted\": true (novox/hq ADR 0348 §8)", d.Predefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
// A template's text is not here yet. Resolve fills it from the build, WithTemplatesFrom from the tree; neither
|
||||
// judges again itself, but WithTemplatesFrom parses its result again, and a resolved build is parsed again
|
||||
// where it is registered, and this is judged then.
|
||||
if !placed[key] && !d.Runtime && !m.Templated() {
|
||||
say("placed nowhere: no file says ${config:%s} or takes it under %s, and it is not marked "+
|
||||
"\"runtime\": true for a value the module's code reads itself (novox/hq ADR 0348 §5)", key, TakesConfigField)
|
||||
}
|
||||
}
|
||||
used := settingKeysUsedBy(m)
|
||||
for _, key := range sortedKeys(used) {
|
||||
if _, declared := m.Settings[key]; !declared {
|
||||
problems = append(problems, fmt.Sprintf("%s says ${config:%s} and declares no such value under %s: "+
|
||||
"declare %q with its type, description and a default or required (novox/hq ADR 0348 §4, §5)",
|
||||
m.Module, key, ConfigField, key))
|
||||
}
|
||||
}
|
||||
problems = append(problems, configTakesProblems(m)...)
|
||||
problems = append(problems, reflectsProblems(m)...)
|
||||
return problems
|
||||
}
|
||||
|
||||
// configPlaced is every value a file, a unit, a directory's owner, a contribution or a served fact asks for, and
|
||||
// every value a mergeable file takes.
|
||||
func configPlaced(m Manifest) map[string]bool {
|
||||
placed := settingKeysUsedBy(m)
|
||||
for _, r := range m.Resources {
|
||||
for _, k := range stringsOf(r[TakesSettingsField]) {
|
||||
placed[k] = true
|
||||
}
|
||||
}
|
||||
return placed
|
||||
}
|
||||
|
||||
// validationProblems is what is wrong with a declaration's validation, and its default against it.
|
||||
func validationProblems(d SettingDeclaration) []string {
|
||||
var problems []string
|
||||
isNumber := d.Type == ConfigNumber
|
||||
if len(d.Enum) > 0 && d.Type != ConfigNumber && d.Type != ConfigString {
|
||||
problems = append(problems, fmt.Sprintf("an enum is for a number or a string, and this is a %s", d.Type))
|
||||
}
|
||||
if (d.Minimum != nil || d.Maximum != nil) && !isNumber {
|
||||
problems = append(problems, fmt.Sprintf("a minimum or maximum is for a number, and this is a %s", d.Type))
|
||||
}
|
||||
if d.Minimum != nil && d.Maximum != nil && *d.Minimum > *d.Maximum {
|
||||
problems = append(problems, fmt.Sprintf("its minimum %v is above its maximum %v", *d.Minimum, *d.Maximum))
|
||||
}
|
||||
var pattern *regexp.Regexp
|
||||
if d.Pattern != "" {
|
||||
if d.Type != ConfigString {
|
||||
problems = append(problems, fmt.Sprintf("a pattern is for a string, and this is a %s", d.Type))
|
||||
}
|
||||
var err error
|
||||
if pattern, err = regexp.Compile(`^(?:` + d.Pattern + `)$`); err != nil {
|
||||
problems = append(problems, fmt.Sprintf("its pattern %q is not a regular expression: %v", d.Pattern, err))
|
||||
}
|
||||
}
|
||||
for _, v := range d.Enum {
|
||||
if !ofType(v, d.Type) {
|
||||
problems = append(problems, fmt.Sprintf("its enum holds %v, which is not a %s", v, d.Type))
|
||||
}
|
||||
}
|
||||
if d.Default == nil {
|
||||
return problems
|
||||
}
|
||||
if !ofType(d.Default, d.Type) {
|
||||
if d.Type != "" {
|
||||
problems = append(problems, fmt.Sprintf("its default %v is not a %s", d.Default, d.Type))
|
||||
}
|
||||
return problems
|
||||
}
|
||||
if len(d.Enum) > 0 && !slices.ContainsFunc(d.Enum, func(v any) bool { return v == d.Default }) {
|
||||
problems = append(problems, fmt.Sprintf("its default %v is not one of %v", d.Default, d.Enum))
|
||||
}
|
||||
if n, ok := d.Default.(float64); ok {
|
||||
if d.Minimum != nil && n < *d.Minimum {
|
||||
problems = append(problems, fmt.Sprintf("its default %v is below its minimum %v", n, *d.Minimum))
|
||||
}
|
||||
if d.Maximum != nil && n > *d.Maximum {
|
||||
problems = append(problems, fmt.Sprintf("its default %v is above its maximum %v", n, *d.Maximum))
|
||||
}
|
||||
}
|
||||
if s, ok := d.Default.(string); ok && pattern != nil && !pattern.MatchString(s) {
|
||||
problems = append(problems, fmt.Sprintf("its default %q does not match its pattern %q", s, d.Pattern))
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// ofType is whether a value read from JSON is of a config type.
|
||||
func ofType(v any, kind string) bool {
|
||||
switch v.(type) {
|
||||
case float64:
|
||||
return kind == ConfigNumber
|
||||
case string:
|
||||
return kind == ConfigString
|
||||
case bool:
|
||||
return kind == ConfigBoolean
|
||||
case map[string]any:
|
||||
return kind == ConfigObject
|
||||
case []any:
|
||||
return kind == ConfigList
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// configTakesProblems is what ADR 0348 §11 adds for a module in the config form: an account fact in
|
||||
// takes-config (the mesh's own words are refused for both forms by TakenSettingsProblems), and a mergeable file
|
||||
// naming no keys where none of the module's does (where one does, TakenSettingsProblems names every one that
|
||||
// does not).
|
||||
func configTakesProblems(m Manifest) []string {
|
||||
var problems, silent []string
|
||||
naming := false
|
||||
for _, r := range m.Resources {
|
||||
if how, _ := r["merge"].(string); how == "" {
|
||||
continue
|
||||
}
|
||||
if _, said := r[TakesSettingsField]; !said {
|
||||
silent = append(silent, fmt.Sprint(r["id"]))
|
||||
continue
|
||||
}
|
||||
naming = true
|
||||
for _, k := range stringsOf(r[TakesSettingsField]) {
|
||||
_, declared := m.Settings[k]
|
||||
switch {
|
||||
case accountFacts[k]:
|
||||
problems = append(problems, fmt.Sprintf("%s's %v names %q in %s, and %q is an account fact the "+
|
||||
"mesh places, never a config value (novox/hq ADR 0348 §11)", m.Module, r["id"], k,
|
||||
TakesConfigField, k))
|
||||
case meshWords[k]:
|
||||
// Refused for both forms by TakenSettingsProblems, in its words.
|
||||
case !declared:
|
||||
problems = append(problems, fmt.Sprintf("%s's %v names %q in %s and declares no such value under "+
|
||||
"%s: a value a file takes is declared like every other, with its type, description and a "+
|
||||
"default or required, or the config verb could never set it (novox/hq ADR 0348 §4, §11)",
|
||||
m.Module, r["id"], k, TakesConfigField, ConfigField))
|
||||
}
|
||||
}
|
||||
}
|
||||
if !naming {
|
||||
for _, id := range silent {
|
||||
problems = append(problems, fmt.Sprintf("%s's mergeable %s names no %s: a module that declares config "+
|
||||
"names the keys each mergeable file takes, an empty list for none (novox/hq ADR 0348 §11)",
|
||||
m.Module, id, TakesConfigField))
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// meshMadeIDs are the resources the mesh composes into a module's own set, which its `restart-on` may name
|
||||
// without declaring them; meshMadePrefixes are those named after what they are for.
|
||||
var (
|
||||
meshMadeIDs = []string{ComposedJailsID(), FilteringID(), CertificateID(), AuthorityID(), BusUsersID()}
|
||||
meshMadePrefixes = []string{NeedID(""), SecretID(""), strings.TrimSuffix(GrantID("", ""), "-"), BoundID(""), ReceivedID(""), AccessID("/")}
|
||||
)
|
||||
|
||||
// reflectsProblems is a `reload-on` or `restart-on` naming a file the module does not declare (ADR 0348 §6): not
|
||||
// one of its own resources, not one the mesh composes for it, and not another module's, which is named
|
||||
// `<module>.<id>`.
|
||||
func reflectsProblems(m Manifest) []string {
|
||||
own := map[string]bool{}
|
||||
for _, r := range m.Resources {
|
||||
own[fmt.Sprint(r["id"])] = true
|
||||
}
|
||||
meshMade := func(id string) bool {
|
||||
if slices.Contains(meshMadeIDs, id) {
|
||||
return true
|
||||
}
|
||||
for _, p := range meshMadePrefixes {
|
||||
if strings.HasPrefix(id, p) && len(id) > len(p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
var problems []string
|
||||
for _, r := range m.Resources {
|
||||
for _, field := range []string{"reload-on", "restart-on"} {
|
||||
for _, id := range stringsOf(r[field]) {
|
||||
if strings.Contains(id, ".") || own[id] || meshMade(id) {
|
||||
continue
|
||||
}
|
||||
problems = append(problems, fmt.Sprintf("%s's %v names %q in its %s, and %s declares no such "+
|
||||
"file: name a file of its own, or another module's as <module>.<id> (novox/hq ADR 0348 §6)",
|
||||
m.Module, r["id"], id, field, m.Module))
|
||||
}
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
Reference in New Issue
Block a user