package catalogue import ( "bytes" "encoding/json" "fmt" "regexp" "sort" "strings" ) // A config value resolves from four sources, the last set winning (novox/hq issue 630): // // 1. the default in the module's manifest; // 2. mesh config: a mesh-wide value for a predefined key, such as `font-size`; // 3. node config: a value for a predefined key for one node; // 4. module-assignment config: that module's own value on that node, the most specific. // // An assignment is one module on one node: there is no module layer for the whole mesh. Mesh and node // config hold values only for the keys on a fixed, reviewable list — the catalogue's `predefined.json` — // that any module may fall back to, by naming one as its preference's `predefined`. A preference without // `predefined` reads only its default and its module-assignment config. A preference is never trusted. // Preference is one value a module reads that a machine may tune, as the manifest declares it under // `preferences`. type Preference struct { // Type is "number", "string" or "boolean". Type string `json:"type"` // Default is the value when no source sets it, of Type. Default any `json:"default"` // Predefined names the key on the catalogue's list this preference falls back to: mesh config, // then node config, for that key. Empty reads only the default and the module-assignment config. Predefined string `json:"predefined,omitempty"` // Why this default: one sentence for whoever wonders whether to change it. Why string `json:"why"` } // The types a preference and a predefined key may have. const ( TypeNumber = "number" TypeString = "string" TypeBoolean = "boolean" ) // PredefinedFile is the catalogue repository's root file holding the predefined keys. const PredefinedFile = "predefined.json" // PredefinedKey is one key mesh and node config may hold a value for. type PredefinedKey struct { Type string `json:"type"` Why string `json:"why"` } // Predefined is the list of predefined keys, by key. type Predefined map[string]PredefinedKey // Keys is the list's keys, sorted. func (p Predefined) Keys() []string { keys := make([]string, 0, len(p)) for k := range p { keys = append(keys, k) } sort.Strings(keys) return keys } // predefinedName is what a predefined key looks like: the shape of a setting key. var predefinedName = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]*$`) // ParsePredefined reads `predefined.json` strictly, and refuses a key whose name, type or why is wrong. func ParsePredefined(raw []byte) (Predefined, error) { decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.DisallowUnknownFields() var list Predefined if err := decoder.Decode(&list); err != nil { return nil, fmt.Errorf("%s is not a list of predefined keys ({\"\": {\"type\": …, \"why\": …}}): %w", PredefinedFile, err) } var problems []string for _, key := range list.Keys() { k := list[key] if !predefinedName.MatchString(key) { problems = append(problems, fmt.Sprintf("%q is not a usable key: lower-case letters, digits, dots, dashes and underscores", key)) } if meshWords[key] { problems = append(problems, fmt.Sprintf("%q is a key the mesh reads itself", key)) } if !knownType(k.Type) { problems = append(problems, fmt.Sprintf("%q has the type %q; a type is %q, %q or %q", key, k.Type, TypeNumber, TypeString, TypeBoolean)) } if strings.TrimSpace(k.Why) == "" { problems = append(problems, fmt.Sprintf("%q has no why: say in one sentence what the key is for", key)) } } if len(problems) > 0 { return nil, fmt.Errorf("%s: %s", PredefinedFile, strings.Join(problems, "; ")) } if list == nil { list = Predefined{} } return list, nil } func knownType(t string) bool { return t == TypeNumber || t == TypeString || t == TypeBoolean } // TypeOf is the type a JSON value has, as a preference names it, or "" for one that is none of them. func TypeOf(v any) string { switch v.(type) { case float64, int, int64, json.Number: return TypeNumber case string: return TypeString case bool: return TypeBoolean } return "" } // PreferencesOf is every preference a module declares: those under `preferences`, and — while the // catalogue moves to them — every `settings` entry of kind preference, its type read from its default. func PreferencesOf(m Manifest) map[string]Preference { if len(m.Preferences) == 0 && len(m.Settings) == 0 { return nil } out := make(map[string]Preference, len(m.Preferences)+len(m.Settings)) for key, d := range m.Settings { out[key] = Preference{Type: TypeOf(d.Default), Default: d.Default, Why: d.Why} } for key, p := range m.Preferences { out[key] = p } return out } // DeclaresPreference is whether a module declares a preference by this key. func DeclaresPreference(m Manifest, key string) bool { _, has := m.Preferences[key] if !has { _, has = m.Settings[key] } return has } // PredefinedProblems is every way a module's preferences disagree with the list of predefined keys: a // `predefined` not on it, or a type other than the list's. func PredefinedProblems(m Manifest, list Predefined) []string { var problems []string keys := make([]string, 0, len(m.Preferences)) for k := range m.Preferences { keys = append(keys, k) } sort.Strings(keys) for _, key := range keys { p := m.Preferences[key] if p.Predefined == "" { continue } k, on := list[p.Predefined] if !on { listed := "the list holds none" if len(list) > 0 { listed = "the list holds " + strings.Join(list.Keys(), ", ") } problems = append(problems, fmt.Sprintf("%s's preference %q falls back to the predefined key %q, which "+ "is not on the catalogue's list of predefined keys (%s; %s)", m.Module, key, p.Predefined, PredefinedFile, listed)) continue } if k.Type != p.Type { problems = append(problems, fmt.Sprintf("%s's preference %q is a %s, and the predefined key %q it falls "+ "back to is a %s", m.Module, key, p.Type, p.Predefined, k.Type)) } } return problems } // Where a value from mesh or node config is said to come from. const MeshConfigLayer = "mesh config" // NodeConfigLayer is what the node config of one node is called where a value's source is said. func NodeConfigLayer(node string) string { return "node config (" + node + ")" } // ConfigLayers are the layers mesh config and node config make for one module: each of its preferences // that names a predefined key, given that key's value. Laid between the module's defaults and its // module-assignment config; like the defaults they fill `${setting:}` only. func ConfigLayers(m Manifest, mesh, node map[string]any, nodeName string) []Layer { var out []Layer for _, source := range []struct { from string values map[string]any }{{MeshConfigLayer, mesh}, {NodeConfigLayer(nodeName), node}} { values := map[string]any{} for key, p := range m.Preferences { if p.Predefined == "" { continue } if v, set := source.values[p.Predefined]; set { values[key] = v } } if len(values) > 0 { out = append(out, Layer{From: source.from, Values: values, Config: true}) } } return out } // ConfigValueProblem refuses a value of the wrong type for a predefined key or a preference. func ConfigValueProblem(what string, want string, v any) error { if got := TypeOf(v); got != want { shown, _ := json.Marshal(v) return fmt.Errorf("%s is a %s, and %s is %s", what, want, shown, aOrNothing(got)) } return nil } func aOrNothing(t string) string { if t == "" { return "none of a number, a string or a boolean" } return "a " + t } // preferenceProblems is every way a module's `preferences` are wrong in themselves. func preferenceProblems(m Manifest) []string { var problems []string keys := make([]string, 0, len(m.Preferences)) for k := range m.Preferences { keys = append(keys, k) } sort.Strings(keys) for _, key := range keys { p := m.Preferences[key] say := func(format string, args ...any) { problems = append(problems, fmt.Sprintf("%s's preference %q: ", m.Module, key)+fmt.Sprintf(format, args...)) } if _, twice := m.Settings[key]; twice { say("declared under settings too; declare it once, under preferences") } if !knownType(p.Type) { say("the type is %q; a type is %q, %q or %q", p.Type, TypeNumber, TypeString, TypeBoolean) } else if p.Default == nil { say("no default: a preference has one") } else if got := TypeOf(p.Default); got != p.Type { shown, _ := json.Marshal(p.Default) say("it is a %s, and its default %s is %s", p.Type, shown, aOrNothing(got)) } if p.Predefined != "" && !predefinedName.MatchString(p.Predefined) { say("%q is not a predefined key's name", p.Predefined) } } return problems } // PredefinedNamed is every predefined key a module's preferences fall back to, sorted. func PredefinedNamed(m Manifest) []string { seen := map[string]bool{} var out []string for _, p := range m.Preferences { if p.Predefined != "" && !seen[p.Predefined] { seen[p.Predefined] = true out = append(out, p.Predefined) } } sort.Strings(out) return out }