Files
mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient/condition.go
T

118 lines
4.2 KiB
Go

package operatorchannelclient
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
)
// ConditionSubject is where a module says a condition to the operator: the seat's by-caller condition accept
// (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("operator-channel: 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("operator-channel: the condition's publisher %q is not one token: it holds a dot, a wildcard or whitespace", publisher)
}
return seat.AcceptSubjectCondition(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("operator-channel: 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"
)
func checkCondition(c seat.Condition) error {
if c.Key == "" {
return errors.New("operator-channel: a condition has no key")
}
if c.Subject.Scope == "" || c.Subject.ID == "" {
return errors.New("operator-channel: a condition's subject needs its scope and id")
}
switch c.State {
case StateCleared:
return nil
case StateRaised:
default:
return fmt.Errorf("operator-channel: the condition's state %q is not raised or cleared", c.State)
}
switch c.Severity {
case SeverityUrgent, SeverityWarning:
default:
return fmt.Errorf("operator-channel: the condition's severity %q is not urgent or warning", c.Severity)
}
if c.Headline == "" {
return errors.New("operator-channel: a raised condition has no headline")
}
if n := len([]rune(c.Headline)); n > HeadlineLength {
return fmt.Errorf("operator-channel: 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("operator-channel: 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 seat.Condition) ([]byte, error) {
if err := checkCondition(c); 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) (seat.Condition, error) {
var probe struct {
V *int `json:"v"`
}
if err := json.Unmarshal(data, &probe); err != nil {
return seat.Condition{}, fmt.Errorf("operator-channel: the condition is not JSON: %w", err)
}
if probe.V == nil {
return seat.Condition{}, fmt.Errorf("%w: no version in the value, this reader reads %d", ErrUnknownConditionVersion, ConditionVersion)
}
if *probe.V != ConditionVersion {
return seat.Condition{}, fmt.Errorf("%w: the value is version %d, this reader reads %d", ErrUnknownConditionVersion, *probe.V, ConditionVersion)
}
var c seat.Condition
if err := json.Unmarshal(data, &c); err != nil {
return seat.Condition{}, fmt.Errorf("operator-channel: the condition does not fit its shape: %w", err)
}
if err := checkCondition(c); err != nil {
return seat.Condition{}, err
}
return c, nil
}
// SameCondition says whether two conditions are the same value.
func SameCondition(a, b seat.Condition) bool { return reflect.DeepEqual(a, b) }