Files
mesh-catalog/modules/mesh-config/cmd/mesh-config/declared.go
T

233 lines
7.2 KiB
Go

package main
import (
"bytes"
"encoding/json"
"fmt"
"math"
"regexp"
"slices"
"sort"
"strings"
)
// What mesh-config resolves from (novox/hq ADR 0348 §2, §4, §7): each module's declarations, the list of predefined
// keys, and the assignments, as the controller mirrors them on the bus (design 57 F3, K3).
// The types a config value may have (ADR 0348 §4).
const (
TypeNumber = "number"
TypeString = "string"
TypeBoolean = "boolean"
TypeObject = "object"
TypeList = "list"
)
// Declaration is one value a module declares, as the declared-config mirror holds it: the config form's fields,
// or the settings form's kind, default and why for a module not yet moved (ADR 0348 §1). The mirror writes kind,
// default and why for every key, so a config-form key comes as kind "", why "" and default null: an empty kind or
// why and a null default are read as not given, never as an empty value or a null default.
type Declaration struct {
Kind string `json:"kind,omitempty"`
Default json.RawMessage `json:"default,omitempty"`
Why string `json:"why,omitempty"`
Type string `json:"type,omitempty"`
Description string `json:"description,omitempty"`
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"`
Predefined string `json:"predefined,omitempty"`
Required bool `json:"required,omitempty"`
Runtime bool `json:"runtime,omitempty"`
}
// HasDefault is whether the declaration gives a default: present and not null.
func (d Declaration) HasDefault() bool {
return len(bytes.TrimSpace(d.Default)) > 0 && string(bytes.TrimSpace(d.Default)) != "null"
}
// TypeOf is the value's type: the config form's, or for the settings form the type of its default.
func (d Declaration) TypeOf() string {
if d.Type != "" {
return d.Type
}
if !d.HasDefault() {
return ""
}
var v any
if json.Unmarshal(d.Default, &v) != nil {
return ""
}
return typeOfValue(v)
}
// ModuleDeclarations is one module's value of the declared-config mirror.
type ModuleDeclarations struct {
Module string `json:"module"`
Config map[string]Declaration `json:"settings"`
}
// ParseModuleDeclarations reads one key of the declared-config mirror.
func ParseModuleDeclarations(raw []byte) (ModuleDeclarations, error) {
var m ModuleDeclarations
if err := json.Unmarshal(raw, &m); err != nil {
return m, fmt.Errorf("not a module's declarations: %w", err)
}
if m.Module == "" {
return m, fmt.Errorf("a module's declarations that name no module")
}
if m.Config == nil {
m.Config = map[string]Declaration{}
}
for key, d := range m.Config {
if !d.HasDefault() {
d.Default = nil
}
m.Config[key] = d
}
return m, nil
}
// PredefinedKey is one key of the list of predefined keys (ADR 0348 §7): its type, description, validation and
// whether it is trusted. It has no default.
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 the controller kept, as its predefined mirror holds it under PredefinedMirrorKey.
type PredefinedList struct {
Keys map[string]PredefinedKey `json:"keys"`
Repository string `json:"repository"`
Commit string `json:"commit"`
Generation int `json:"generation"`
Build string `json:"build"`
}
// PredefinedMirrorKey is the controller's predefined mirror's one key.
const PredefinedMirrorKey = "list"
// Published is one key of mesh-config_predefined: the key's declaration, and the build of the list it came from.
type Published struct {
PredefinedKey
Generation int `json:"generation"`
Commit string `json:"commit"`
}
// Assignment is one key of the assignments mirror: a module on a node.
type Assignment struct {
Node string `json:"node"`
Module string `json:"module"`
}
// rule is the validation a value is held to, from a module's declaration or a predefined key.
type rule struct {
Type string
Enum []any
Minimum *float64
Maximum *float64
Pattern string
}
func (d Declaration) rule() rule {
return rule{Type: d.TypeOf(), Enum: d.Enum, Minimum: d.Minimum, Maximum: d.Maximum, Pattern: d.Pattern}
}
func (k PredefinedKey) rule() rule {
return rule{Type: k.Type, Enum: k.Enum, Minimum: k.Minimum, Maximum: k.Maximum, Pattern: k.Pattern}
}
// check is every way value breaks the rule, said in a sentence each; none for a value that holds.
func (r rule) check(value any) []string {
var problems []string
if r.Type != "" && typeOfValue(value) != r.Type {
return []string{fmt.Sprintf("%s is not a %s", show(value), r.Type)}
}
if len(r.Enum) > 0 && !slices.ContainsFunc(r.Enum, func(e any) bool { return sameValue(e, value) }) {
problems = append(problems, fmt.Sprintf("%s is not one of %s", show(value), show(r.Enum)))
}
if n, ok := value.(float64); ok {
if r.Minimum != nil && n < *r.Minimum {
problems = append(problems, fmt.Sprintf("%s is below the minimum %s", show(n), show(*r.Minimum)))
}
if r.Maximum != nil && n > *r.Maximum {
problems = append(problems, fmt.Sprintf("%s is above the maximum %s", show(n), show(*r.Maximum)))
}
if math.IsNaN(n) || math.IsInf(n, 0) {
problems = append(problems, "a number is finite")
}
}
if s, ok := value.(string); ok && r.Pattern != "" {
pattern, err := regexp.Compile(`^(?:` + r.Pattern + `)$`)
switch {
case err != nil:
problems = append(problems, fmt.Sprintf("its pattern %q cannot be read: %v", r.Pattern, err))
case !pattern.MatchString(s):
problems = append(problems, fmt.Sprintf("%q does not match the pattern %q", s, r.Pattern))
}
}
return problems
}
// typeOfValue is the config type of a value read from JSON.
func typeOfValue(v any) string {
switch v.(type) {
case float64:
return TypeNumber
case string:
return TypeString
case bool:
return TypeBoolean
case map[string]any:
return TypeObject
case []any:
return TypeList
}
return ""
}
func sameValue(a, b any) bool {
ra, _ := json.Marshal(a)
rb, _ := json.Marshal(b)
return bytes.Equal(ra, rb)
}
func show(v any) string {
raw, err := json.Marshal(v)
if err != nil {
return fmt.Sprint(v)
}
return string(raw)
}
// keyName is what a config key may be (the catalogue's rule): lower-case letters, digits, dots, dashes and
// underscores, starting with a letter or digit.
var keyName = regexp.MustCompile(`^[a-z0-9][a-z0-9_.-]*$`)
// nodeOrModule is what a node or a module name may be: one token of a bucket's key.
var nodeOrModule = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
func sortedKeys[V any](m map[string]V) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
sort.Strings(out)
return out
}
func joinKeys[V any](m map[string]V) string {
keys := sortedKeys(m)
if len(keys) == 0 {
return "none"
}
return strings.Join(keys, ", ")
}