118 lines
4.2 KiB
Go
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) }
|