Files
mesh-sdk/go/catalogue/predefined.go
T

190 lines
6.8 KiB
Go

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)
}