233 lines
7.2 KiB
Go
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, ", ")
|
|
}
|