node-engine client: the node key of the have, the codecs of the health report and of the data items (ADR 0634 §1, §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -105,3 +105,46 @@ func TestHaveNodeCodec(t *testing.T) {
|
||||
t.Error("a record differs by its version")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthAndDataCodecs(t *testing.T) {
|
||||
at := time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC)
|
||||
h := seat.HealthByNodeValue{Node: "n", At: at, Fallback: &seat.LoginFallback{Node: "n", From: "old", Why: "w", At: at}}
|
||||
raw, err := EncodeHealth(h)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
back, err := DecodeHealth(raw)
|
||||
h.V = keys.Version
|
||||
if err != nil || !SameHealth(back, h) {
|
||||
t.Fatalf("round trip: %v %+v", err, back)
|
||||
}
|
||||
later := h
|
||||
later.At = at.Add(time.Hour)
|
||||
if !SameHealth(h, later) {
|
||||
t.Error("a report differs by its time of writing")
|
||||
}
|
||||
for name, bad := range map[string]seat.HealthByNodeValue{
|
||||
"no node": {},
|
||||
"another node": {Node: "n", Fallback: &seat.LoginFallback{Node: "m", From: "o", Why: "w", At: at}},
|
||||
"no time": {Node: "n", Fallback: &seat.LoginFallback{Node: "n", From: "o", Why: "w"}},
|
||||
"no login from": {Node: "n", Fallback: &seat.LoginFallback{Node: "n", Why: "w", At: at}},
|
||||
} {
|
||||
if _, err := EncodeHealth(bad); err == nil {
|
||||
t.Errorf("%s: encoded", name)
|
||||
}
|
||||
}
|
||||
if _, err := DecodeHealth([]byte(`{"v":2,"node":"n"}`)); !errors.Is(err, keys.ErrUnknownVersion) {
|
||||
t.Errorf("version 2: %v", err)
|
||||
}
|
||||
d := seat.DataByNodeModuleValue{Node: "n", Module: "m", Digest: digestA}
|
||||
raw, err = EncodeData(d)
|
||||
if err != nil || !strings.Contains(string(raw), `"items":[]`) {
|
||||
t.Fatalf("data: %v %s", err, raw)
|
||||
}
|
||||
if _, err := EncodeData(seat.DataByNodeModuleValue{Node: "n", Module: "node"}); err == nil {
|
||||
t.Error("the module name node keys")
|
||||
}
|
||||
if k, err := HaveNodeKey("n"); err != nil || k != "n.node" {
|
||||
t.Errorf("node key %q %v", k, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package nodeengineclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
)
|
||||
|
||||
// A node's own health report (the state health, one key per node, written by that node's engine alone) and the data items
|
||||
// a module declares as the engine placed them (the state data, `<node>.<module>`): their layouts are the seat's contract
|
||||
// (seat.HealthByNodeValue, seat.DataByNodeModuleValue); this file is their codec, shared by the engine and its readers.
|
||||
|
||||
// EncodeHealth is h as JSON at the current version; it refuses a report naming no node, and a fall back to the old login
|
||||
// that names another node, no login fallen back from, no reason or no time.
|
||||
func EncodeHealth(h seat.HealthByNodeValue) ([]byte, error) {
|
||||
h.V = keys.Version
|
||||
if h.Node == "" {
|
||||
return nil, fmt.Errorf("node-engine: a node's health report names its node")
|
||||
}
|
||||
if f := h.Fallback; f != nil && (f.Node != h.Node || f.From == "" || f.Why == "" || f.At.IsZero()) {
|
||||
return nil, fmt.Errorf("node-engine: a node's fall back to its old login names its node %s, the login fallen back from, why and when", h.Node)
|
||||
}
|
||||
return json.Marshal(h)
|
||||
}
|
||||
|
||||
// DecodeHealth reads a report; it refuses another version (keys.ErrUnknownVersion) and a field the contract does not know.
|
||||
func DecodeHealth(raw []byte) (seat.HealthByNodeValue, error) {
|
||||
if err := keys.CheckVersion(raw); err != nil {
|
||||
return seat.HealthByNodeValue{}, err
|
||||
}
|
||||
h, err := seat.DecodeHealthByNodeValue(raw)
|
||||
if err != nil {
|
||||
return seat.HealthByNodeValue{}, fmt.Errorf("node-engine: a node's health report is not readable: %w", err)
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// SameHealth says two reports say the same, whenever each was written.
|
||||
func SameHealth(a, b seat.HealthByNodeValue) bool {
|
||||
a.At, b.At = time.Time{}, time.Time{}
|
||||
return sameJSON(a, b)
|
||||
}
|
||||
|
||||
// DataKey is a module's key on a node in the state data, `<node>.<module>`, its module part escaped as a have's is.
|
||||
func DataKey(node, module string) (string, error) { return HaveKey(node, module) }
|
||||
|
||||
// DataNodeKeys is the pattern of every data key of a node, `<node>.*`.
|
||||
func DataNodeKeys(node string) (string, error) {
|
||||
if err := keys.CheckName("node", node); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return node + ".*", nil
|
||||
}
|
||||
|
||||
// EncodeData is d as JSON at the current version; it refuses a node or module the key refuses.
|
||||
func EncodeData(d seat.DataByNodeModuleValue) ([]byte, error) {
|
||||
d.V = keys.Version
|
||||
if _, err := DataKey(d.Node, d.Module); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d.Items == nil {
|
||||
d.Items = []seat.PlacedDataItem{}
|
||||
}
|
||||
return json.Marshal(d)
|
||||
}
|
||||
|
||||
// DecodeData reads a module's data items; it refuses another version (keys.ErrUnknownVersion).
|
||||
func DecodeData(raw []byte) (seat.DataByNodeModuleValue, error) {
|
||||
if err := keys.CheckVersion(raw); err != nil {
|
||||
return seat.DataByNodeModuleValue{}, err
|
||||
}
|
||||
d, err := seat.DecodeDataByNodeModuleValue(raw)
|
||||
if err != nil {
|
||||
return seat.DataByNodeModuleValue{}, fmt.Errorf("node-engine: a module's data items are not readable: %w", err)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
@@ -120,3 +120,11 @@ func ParseComposedKey(key string) (node, module, fingerprintPart string, isNode
|
||||
}
|
||||
return refused(fmt.Errorf("the key has %d parts", len(parts)))
|
||||
}
|
||||
|
||||
// HaveNodeKey is the key, in the state have, of a node's own facts: `<node>.node`.
|
||||
func HaveNodeKey(node string) (string, error) {
|
||||
if err := keys.CheckName("node", node); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return node + "." + NodeWord, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user