package catalogue import ( "bytes" "encoding/json" "errors" "fmt" "io/fs" "os" "path/filepath" "sort" "strings" "sync" ) // PredefinedFile is the list's file at the root of the catalogue's repository. const PredefinedFile = "predefined.json" // PredefinedKey is one key of the list: its type, its description, its validation and whether it is trusted // (ADR 0348 §7). It has no default: a module's value falling back to it has its own. type PredefinedKey struct { Type string `json:"type"` Description string `json:"description"` 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"` } // PredefinedList is the list by key. Nil is no list at all, which is not the same as an empty one: a value // naming a key is refused then saying the list is not present. type PredefinedList map[string]PredefinedKey // ParsePredefined reads predefined.json strictly: one JSON object of keys, each with exactly the fields of a // PredefinedKey, and every key judged as a module's config value is (PredefinedProblems). Anything else is // refused whole, naming each problem — a list half read would let a module fall back to a key nobody reviewed. func ParsePredefined(raw []byte) (PredefinedList, error) { var keys map[string]json.RawMessage if err := json.Unmarshal(raw, &keys); err != nil { return nil, fmt.Errorf("%s is not a JSON object of keys: %w", PredefinedFile, err) } if keys == nil { return nil, fmt.Errorf("%s is not a JSON object of keys: it says null", PredefinedFile) } list := PredefinedList{} var problems []string for _, key := range sortedKeys(keys) { decoder := json.NewDecoder(bytes.NewReader(keys[key])) decoder.DisallowUnknownFields() var k PredefinedKey if err := decoder.Decode(&k); err != nil { problems = append(problems, fmt.Sprintf("%q is not a predefined key's declaration (type, description, "+ "enum, minimum, maximum, pattern, trusted): %v", key, err)) continue } list[key] = k } problems = append(problems, PredefinedProblems(list)...) if len(problems) > 0 { return nil, fmt.Errorf("%s is refused: %s", PredefinedFile, strings.Join(problems, "; ")) } return list, nil } // PredefinedProblems is every way a key of the list is declared wrongly, by the rules a module's config value // is held to (ADR 0348 §4, §7, §11): a usable name that is not placement, a type, a description, validation // that fits the type, and an object or a list only when trusted. func PredefinedProblems(list PredefinedList) []string { var problems []string for _, key := range sortedKeys(list) { k := list[key] say := func(format string, args ...any) { problems = append(problems, fmt.Sprintf("the predefined key %q: ", 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 } switch k.Type { case ConfigNumber, ConfigString, ConfigBoolean: case ConfigObject, ConfigList: if !k.Trusted { say("is %s and not trusted: an object or a list is trusted, \"trusted\": true (novox/hq ADR 0348 §4)", k.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)", k.Type) } if strings.TrimSpace(k.Description) == "" { say("no description: say in a sentence what the value is, including its unit (novox/hq ADR 0348 §7)") } for _, wrong := range validationProblems(SettingDeclaration{Type: k.Type, Enum: k.Enum, Minimum: k.Minimum, Maximum: k.Maximum, Pattern: k.Pattern}) { say("%s", wrong) } } return problems } // ReadPredefined is the list in predefined.json at a repository's root, and nil with no error when the root // holds none: a repository other than the catalogue, or the catalogue before the file. func ReadPredefined(root string) (PredefinedList, error) { path := filepath.Join(root, PredefinedFile) info, err := os.Lstat(path) if errors.Is(err, fs.ErrNotExist) { return nil, nil } if err != nil { return nil, err } if !info.Mode().IsRegular() { return nil, fmt.Errorf("%s is %s, not a plain file", path, kindOf(info.Mode())) } raw, err := os.ReadFile(path) if err != nil { return nil, err } return ParsePredefined(raw) } // Keys is the list's keys, sorted. func (l PredefinedList) Keys() []string { out := make([]string, 0, len(l)) for k := range l { out = append(out, k) } sort.Strings(out) return out } // publishedPredefined is the list the mesh published, which a manifest is judged against where no tree is read. var publishedPredefined struct { sync.RWMutex list PredefinedList } // PublishedPredefined is the list a manifest is judged against where no tree is read; nil when none is loaded. func PublishedPredefined() PredefinedList { publishedPredefined.RLock() defer publishedPredefined.RUnlock() return publishedPredefined.list } // ReplacePublished sets the published list, nil included, and answers the one it replaced: for a test that // judges against a list of its own and puts the mesh's back. func ReplacePublished(list PredefinedList) (was PredefinedList) { publishedPredefined.Lock() defer publishedPredefined.Unlock() was, publishedPredefined.list = publishedPredefined.list, list return was } // ReadTreeManifestWritten is the manifest in a module's directory of a repository tree as written, still naming its // templates, decoded and judged by nothing (novox/hq ADR 0578 §3: the indexer's judge judges manifests), the tree's // list of predefined keys read so a tree whose list does not read is still refused. What a build resolves, given the // text it reads back from the archive it packs the templates into (Built.Texts, ReadTemplates). func ReadTreeManifestWritten(root, dir string) (Manifest, error) { list, err := ReadPredefined(root) if err != nil { return Manifest{}, err } raw, err := os.ReadFile(filepath.Join(dir, "module.json")) if err != nil { return Manifest{}, err } m, err := Decode(raw) if err != nil { return Manifest{}, err } m.predefined = list return m, nil } // ReadTreeManifest is ReadTreeManifestWritten with each template's text read from the module's directory as the // merge gate reads it (WithTemplatesFrom). For a check that composes a tree's manifests, so what it judges is what // the real check judges. func ReadTreeManifest(root, dir string) (Manifest, error) { m, err := ReadTreeManifestWritten(root, dir) if err != nil { return Manifest{}, err } return WithTemplatesFrom(m, dir) }