190 lines
6.8 KiB
Go
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)
|
|
}
|