package catalogue import ( "fmt" "regexp" "sort" "strings" ) // An operator's value, where a definition needs one (novox/hq ADR 0112, ADR 0155, design 27). // // A mail server's domain, a site's name, the address a proxy forwards from: values that are true of // one installation and of no other, and that a module's software must be told. They had nowhere to // live but the definition, which is how a catalogue meant for any mesh came to name this one // (novox/hq issues 122, 134). ADR 0112 names the operator as one of the four providers; this is the // operator answering. // // `${setting:}` in a file's content is filled from the module's settings layers — the mesh's, // then this node's — the same layers a mergeable JSON file and a contribution already take, so // `settings set ` is the one place a person's values go. **Refused when no layer sets it**, // naming the key and the remedy: a definition that carried a default for a mail domain would be // carrying the very literal this removes, and a blank written silently would be a service that // comes up wrong somewhere that names neither the module nor the key. // settingRef is how a definition asks for an operator's value: ${setting:}. var settingRef = regexp.MustCompile(`\$\{setting:([a-z0-9][a-z0-9_.-]*)\}`) // settingsUsed is every key a file's content asks for, once each, in order of first use. func settingsUsed(content string) []string { var keys []string seen := map[string]bool{} for _, m := range settingRef.FindAllStringSubmatch(content, -1) { if !seen[m[1]] { seen[m[1]] = true keys = append(keys, m[1]) } } return keys } // settingInto fills a file's ${setting:…} placeholders from the layers over a module. // // The last layer setting a key wins, which is the node's over the mesh's — the same order settle // applies to a mergeable file. A value that is not a string is written the way a program would read // it (a number without a trailing .000000, a boolean as true/false). func settingInto(resource map[string]any, layers []Layer, module string) error { if fmt.Sprint(resource["type"]) != "file" { return nil } content, ok := resource["content"].(string) if !ok { return nil } for _, key := range settingsUsed(content) { value, set := settingValue(layers, key) if !set { return fmt.Errorf( "%s has a file that says ${setting:%s}, and nothing sets %q for it — an operator's "+ "value is the assignment's, never the definition's (novox/hq ADR 0112): "+ "`settings set %s ` with {%q: …}%s", module, key, key, module, key, orNoSettings(layers)) } content = strings.ReplaceAll(content, "${setting:"+key+"}", plainly(value)) } resource["content"] = content return nil } func settingValue(layers []Layer, key string) (any, bool) { var value any set := false for _, layer := range layers { if v, has := layer.Values[key]; has { value, set = v, true } } return value, set } func orNoSettings(layers []Layer) string { var keys []string for _, l := range layers { for k := range l.Values { keys = append(keys, k) } } if len(keys) == 0 { return "; no setting is set for this module" } sort.Strings(keys) return "; set today: " + strings.Join(keys, ", ") } // settingKeysUsedBy is every key a module's files, contributions and served facts ask for, so a // setting that lands in one is not called stray. func settingKeysUsedBy(m Manifest) map[string]bool { used := map[string]bool{} note := func(s string) { for _, k := range settingsUsed(s) { used[k] = true } } for _, r := range m.Resources { if fmt.Sprint(r["type"]) != "file" { continue } if content, ok := r["content"].(string); ok { note(content) } } inValues := func(values map[string]any) { for _, v := range values { if s, ok := v.(string); ok { note(s) } } } for _, values := range m.Contributes { inValues(values) } for _, locals := range m.ContributesMany { for _, values := range locals { inValues(values) } } for _, values := range m.Serves { inValues(values) } return used }