150 lines
5.3 KiB
Go
150 lines
5.3 KiB
Go
package asks
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
// ConditionSubject is where a module says a condition to the operator: a by-caller accept of the operator-channel
|
|
// seat, `mesh.seat.operator-channel.accept.condition.<publisher>` (novox/hq ADR 0437 §9). The router reads the
|
|
// verb at the fifth part and the caller at the sixth, so the publisher is one token, the module's own name; it is
|
|
// refused when empty or when it holds a dot, a wildcard or whitespace.
|
|
func ConditionSubject(publisher string) (string, error) {
|
|
if publisher == "" {
|
|
return "", errors.New("asks: the condition's publisher is empty")
|
|
}
|
|
if strings.ContainsAny(publisher, ".*> \t\r\n") || strings.ContainsFunc(publisher, func(r rune) bool { return r <= ' ' }) {
|
|
return "", fmt.Errorf("asks: the condition's publisher %q is not one token: it holds a dot, a wildcard or whitespace", publisher)
|
|
}
|
|
return "mesh.seat." + Seat + ".accept.condition." + publisher, nil
|
|
}
|
|
|
|
// ConditionVersion is the version of the condition value this package writes and the only one it reads.
|
|
const ConditionVersion = 1
|
|
|
|
// ErrUnknownConditionVersion is what DecodeCondition returns for a value whose version is missing or not
|
|
// ConditionVersion. Test it with errors.Is.
|
|
var ErrUnknownConditionVersion = errors.New("asks: unknown condition version")
|
|
|
|
// The words a condition's explanation opens with (novox/hq ADR 0253 §1).
|
|
const (
|
|
NothingToDo = "Nothing for you to do."
|
|
NeedsYou = "Needs you: "
|
|
)
|
|
|
|
// The severities and states of a condition.
|
|
const (
|
|
SeverityUrgent = "urgent"
|
|
SeverityWarning = "warning"
|
|
StateRaised = "raised"
|
|
StateCleared = "cleared"
|
|
)
|
|
|
|
// Scope is what a condition is about, under the controller's names.
|
|
type Scope struct {
|
|
Scope string `json:"scope"`
|
|
ID string `json:"id"`
|
|
Machine string `json:"machine,omitempty"`
|
|
}
|
|
|
|
// Action is one answer a notification offers, under the controller's names.
|
|
type Action struct {
|
|
Label string `json:"label"`
|
|
Verb string `json:"verb"`
|
|
Machine string `json:"machine,omitempty"`
|
|
Arguments map[string]string `json:"arguments,omitempty"`
|
|
Level Level `json:"level,omitempty"`
|
|
}
|
|
|
|
// Condition is a condition a module says to the operator, under the names of the controller's conditions: its key,
|
|
// severity, subject, state (raised or cleared), summary and the plain words of ADR 0253. A cleared condition needs
|
|
// only its key, subject and state. The router alone chooses the channels.
|
|
type Condition struct {
|
|
V int `json:"v"`
|
|
Key string `json:"key"`
|
|
Severity string `json:"severity,omitempty"`
|
|
Subject Scope `json:"subject"`
|
|
State string `json:"state"`
|
|
Summary string `json:"summary,omitempty"`
|
|
Headline string `json:"headline,omitempty"`
|
|
Needs string `json:"needs,omitempty"`
|
|
Explanation string `json:"explanation,omitempty"`
|
|
Resolved string `json:"resolved,omitempty"`
|
|
Actions []Action `json:"actions,omitempty"`
|
|
}
|
|
|
|
func (c Condition) check() error {
|
|
if c.Key == "" {
|
|
return errors.New("asks: a condition has no key")
|
|
}
|
|
if c.Subject.Scope == "" || c.Subject.ID == "" {
|
|
return errors.New("asks: a condition's subject needs its scope and id")
|
|
}
|
|
switch c.State {
|
|
case StateCleared:
|
|
return nil
|
|
case StateRaised:
|
|
default:
|
|
return fmt.Errorf("asks: the condition's state %q is not raised or cleared", c.State)
|
|
}
|
|
switch c.Severity {
|
|
case SeverityUrgent, SeverityWarning:
|
|
default:
|
|
return fmt.Errorf("asks: the condition's severity %q is not urgent or warning", c.Severity)
|
|
}
|
|
if c.Headline == "" {
|
|
return errors.New("asks: a raised condition has no headline")
|
|
}
|
|
if n := len([]rune(c.Headline)); n > HeadlineLength {
|
|
return fmt.Errorf("asks: the condition's headline is %d characters, more than %d", n, HeadlineLength)
|
|
}
|
|
verdict := NothingToDo
|
|
if c.Needs != "" {
|
|
verdict = NeedsYou + c.Needs
|
|
}
|
|
if !strings.HasPrefix(c.Explanation, verdict) {
|
|
return fmt.Errorf("asks: the condition's explanation must open with %q", verdict)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeCondition is c as JSON at ConditionVersion; it refuses what ADR 0253 and ADR 0437 §9 refuse.
|
|
func EncodeCondition(c Condition) ([]byte, error) {
|
|
if err := c.check(); err != nil {
|
|
return nil, err
|
|
}
|
|
c.V = ConditionVersion
|
|
return json.Marshal(c)
|
|
}
|
|
|
|
// DecodeCondition reads a condition; it refuses an unknown version (ErrUnknownConditionVersion) and what
|
|
// EncodeCondition refuses.
|
|
func DecodeCondition(data []byte) (Condition, error) {
|
|
var probe struct {
|
|
V *int `json:"v"`
|
|
}
|
|
if err := json.Unmarshal(data, &probe); err != nil {
|
|
return Condition{}, fmt.Errorf("asks: the condition is not JSON: %w", err)
|
|
}
|
|
if probe.V == nil {
|
|
return Condition{}, fmt.Errorf("%w: no version in the value, this reader reads %d", ErrUnknownConditionVersion, ConditionVersion)
|
|
}
|
|
if *probe.V != ConditionVersion {
|
|
return Condition{}, fmt.Errorf("%w: the value is version %d, this reader reads %d", ErrUnknownConditionVersion, *probe.V, ConditionVersion)
|
|
}
|
|
var c Condition
|
|
if err := json.Unmarshal(data, &c); err != nil {
|
|
return Condition{}, fmt.Errorf("asks: the condition does not fit its shape: %w", err)
|
|
}
|
|
if err := c.check(); err != nil {
|
|
return Condition{}, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// SameCondition says whether two conditions are the same value.
|
|
func SameCondition(a, b Condition) bool { return reflect.DeepEqual(a, b) }
|