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:}` 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" // takenKeys is the keys a resource names in takes-settings, 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]bool, bool) { raw, said := resource[TakesSettingsField] if !said { return nil, false } keys := map[string]bool{} for _, k := range stringsOf(raw) { keys[k] = true } return keys, true } // onlyTaken is the layers with every key the file does not name left out. The file's own defaults and // what the mesh reads itself are not touched here: settle already keeps both out of a file. func onlyTaken(layers []Layer, taken map[string]bool) []Layer { kept := make([]Layer, 0, len(layers)) for _, layer := range layers { values := map[string]any{} for key, value := range layer.Values { if taken[key] { values[key] = value } } kept = append(kept, Layer{From: layer.From, Values: values, Default: layer.Default}) } return kept } // 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 { 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 := map[string]bool{} for _, item := range list { key, isString := item.(string) switch { case !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)) case seen[key]: problems = append(problems, fmt.Sprintf("%s's %s names %q twice in %s", m.Module, id, key, TakesSettingsField)) case meshWords[key]: problems = append(problems, fmt.Sprintf("%s's %s names %q in %s, and the mesh reads %q itself "+ "(novox/hq ADR 0164 §14)", m.Module, id, key, TakesSettingsField, key)) case protected[key]: 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", m.Module, id, key, TakesSettingsField)) } if isString { seen[key] = true } } } 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 } // 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:}`: 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 := stringsOf(r[TakesSettingsField]) 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, "; ") + ")" }