Revert the revert of G's mesh-sdk merge (#191): G's sdk stands again over reader release 3
Main reverted G's half merge (#189, reverted by8afc3ebin #191) and then took the reader release's S1-S5, which copied G's code. G's form holds where they differ (process_bus.go, seats.go, state.go and their tests); the release's kept names stay so main's callers keep building: SeatViewPrefix beside SeatReadPrefix, and QueueInvoke's empty strings on not-ok (with its test TestQueueInvoke). The tree differs from G's headf30dee54only by those and the release's two tests (objects cap option, seat states and stream kinds). Builds: design 47 G (the milestone's sdk part, as atf30dee54) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
package operatorask
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// An Act is what an option of an ask binds (novox/hq ADR 0286 §10, §11): named fields, each a string — the verb, the
|
||||
// machine, the level, and each argument under a name of its own ("arg.delivery"). Flat on purpose: its digest is over
|
||||
// these names and values alone, never over how a language happens to encode a struct. It is mesh-sdk's (design 47 G
|
||||
// lead answers A-198, A-219), so the seat server's gate in go/process digests an act as every asker does;
|
||||
// operatorchannelclient re-exports it.
|
||||
type Act map[string]string
|
||||
|
||||
// ActDigest is the digest an asker puts in an option's binds: SHA-256 over the act's canonical encoding, written
|
||||
// "sha256:<hex>". The same names and values give the same digest in any language, whatever order they were set in; a
|
||||
// field renamed, added or emptied gives another.
|
||||
func ActDigest(act Act) (string, error) {
|
||||
if len(act) == 0 {
|
||||
return "", fmt.Errorf("the act cannot be digested: it names nothing")
|
||||
}
|
||||
names := make([]string, 0, len(act))
|
||||
for k := range act {
|
||||
if k == "" {
|
||||
return "", fmt.Errorf("the act cannot be digested: a field has no name")
|
||||
}
|
||||
names = append(names, k)
|
||||
}
|
||||
sort.Strings(names)
|
||||
var b strings.Builder
|
||||
b.WriteString("novox.act.v1\n")
|
||||
for _, k := range names {
|
||||
canonical(&b, k)
|
||||
canonical(&b, act[k])
|
||||
}
|
||||
sum := sha256.Sum256([]byte(b.String()))
|
||||
return "sha256:" + hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
|
||||
// canonical writes one value as its length in bytes, a colon, the bytes and a newline: no value can be read as
|
||||
// another's end or start, so two different sequences of values never encode the same.
|
||||
func canonical(b *strings.Builder, v string) {
|
||||
b.WriteString(strconv.Itoa(len(v)))
|
||||
b.WriteByte(':')
|
||||
b.WriteString(v)
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package operatorask
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Design 47 G A-198: the act's form and its digest are mesh-sdk's, so a seat server in go/process digests an act as
|
||||
// every asker does, with no import of the contracts; the digest is canonical over the act's named fields.
|
||||
func TestTheActsDigestIsCanonicalOverItsNamedFields(t *testing.T) {
|
||||
a1, _ := ActDigest(Act{"verb": "release", "arg.delivery": "hq"})
|
||||
b := Act{}
|
||||
b["arg.delivery"] = "hq"
|
||||
b["verb"] = "release"
|
||||
if a2, _ := ActDigest(b); a1 != a2 {
|
||||
t.Error("the order an act was built in changed its digest")
|
||||
}
|
||||
x, _ := ActDigest(Act{"a": "b\n1:c"})
|
||||
y, _ := ActDigest(Act{"a": "b", "c": ""})
|
||||
if x == y {
|
||||
t.Error("two different acts encode the same")
|
||||
}
|
||||
if _, err := ActDigest(Act{}); err == nil {
|
||||
t.Error("an empty act was digested")
|
||||
}
|
||||
if _, err := ActDigest(Act{"": "x"}); err == nil {
|
||||
t.Error("an act with a nameless field was digested")
|
||||
}
|
||||
sum := sha256.Sum256([]byte("novox.act.v1\n4:verb\n7:release\n"))
|
||||
if got, _ := ActDigest(Act{"verb": "release"}); got != "sha256:"+hex.EncodeToString(sum[:]) {
|
||||
t.Errorf("the act's encoding is not the one written down: %s", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Package operatorask is a module's asking of a person through the seat that routes asks to the operator (design 47 G
|
||||
// A-101, §700, A-74): the seat's holder is read by seatlist.HolderOf, an ask is published under the module's own name, the
|
||||
// router's record of it is read for the warrant, and the router's `decided` is heard. It is shared code, so no module
|
||||
// keeps its own copy; it names no seat, verb or module: the generated contracts' names and types are its parameters.
|
||||
package operatorask
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
)
|
||||
|
||||
// Caller calls a seat verb or module tool as `seat:<seat>.<verb>` or `<module>.<tool>` and returns its answer
|
||||
// (stdio.Ask is one; so is a process's calls).
|
||||
type Caller = seatlist.HolderCaller
|
||||
|
||||
// Contract is what the asker takes from the generated contracts: the asking seat and its state of the router's records.
|
||||
type Contract struct {
|
||||
Seat string
|
||||
// StateAsks is the seat's state holding the router's record of each ask.
|
||||
StateAsks string
|
||||
}
|
||||
|
||||
// Bucket is the bucket of the router's records: the seat's state in the bucket of the seat's holder, as seatlist reads it
|
||||
// (design 47 G A-126).
|
||||
func Bucket(call Caller, c Contract) (string, error) {
|
||||
module, err := seatlist.HolderOf(call, c.Seat)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("the router's record of an ask is not read: %w", err)
|
||||
}
|
||||
return module + "_" + c.StateAsks, nil
|
||||
}
|
||||
|
||||
// Asker asks as one module. W is the generated contract's warrant type.
|
||||
type Asker[W any] struct {
|
||||
Module string
|
||||
Contract Contract
|
||||
// Call reaches the seat list's verb.
|
||||
Call Caller
|
||||
// Record reads one record of a bucket under the module's own name (process.Conn.Ask).
|
||||
Record func(bucket, id string) (*stdio.StateEntry, error)
|
||||
// Publish publishes a body de-duplicated by id: the same id twice is one message (stdio.SeatPublish).
|
||||
Publish func(subject string, body any, id string) error
|
||||
// Subscribe hears an event (stdio.Subscribe).
|
||||
Subscribe func(pattern string, handle func(stdio.Envelope) error) error
|
||||
// AskSubject and DecidedSubject are the generated contract's subjects for Module.
|
||||
AskSubject, DecidedSubject func(module string) string
|
||||
}
|
||||
|
||||
// Ask publishes the ask (the generated contract's Ask) under the module's own name, de-duplicated by its id.
|
||||
func (a *Asker[W]) Ask(id string, ask any) error {
|
||||
if a.Publish == nil || a.AskSubject == nil {
|
||||
return errors.New("the asker has no way to publish")
|
||||
}
|
||||
return a.Publish(a.AskSubject(a.Module), ask, id)
|
||||
}
|
||||
|
||||
// Where is an ask's state as the router keeps it, and its warrant once there is one; a record not yet there is an empty
|
||||
// state. It is an error while the seat's holder or the record cannot be read.
|
||||
func (a *Asker[W]) Where(id string) (state string, warrant *W, err error) {
|
||||
bucket, err := Bucket(a.Call, a.Contract)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
e, err := a.Record(bucket, id)
|
||||
if err != nil || e == nil {
|
||||
return "", nil, err
|
||||
}
|
||||
var rec struct {
|
||||
State string `json:"state"`
|
||||
Warrant *W `json:"warrant"`
|
||||
}
|
||||
if err := json.Unmarshal(e.Value, &rec); err != nil {
|
||||
return "", nil, fmt.Errorf("the router's record of %s cannot be read: %w", id, err)
|
||||
}
|
||||
return rec.State, rec.Warrant, nil
|
||||
}
|
||||
|
||||
// Hear hands handle each warrant the router says to this module; a handler's error leaves it for the bus to give again.
|
||||
func (a *Asker[W]) Hear(handle func(W) error) error {
|
||||
if a.Subscribe == nil || a.DecidedSubject == nil {
|
||||
return errors.New("the asker has no way to hear")
|
||||
}
|
||||
return a.Subscribe(a.DecidedSubject(a.Module), func(env stdio.Envelope) error {
|
||||
var w W
|
||||
if err := json.Unmarshal(env.Body, &w); err != nil {
|
||||
return fmt.Errorf("%s holds what is no warrant: %w", env.Key, err)
|
||||
}
|
||||
return handle(w)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package operatorask
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
)
|
||||
|
||||
type warrant struct {
|
||||
Ask string `json:"ask"`
|
||||
Choice string `json:"choice"`
|
||||
}
|
||||
|
||||
// Design 47 G A-101: one asker finds the seat's holder, publishes, reads the warrant and hears decided.
|
||||
func TestTheAskerFindsTheHolderPublishesReadsTheWarrantAndHearsDecided(t *testing.T) {
|
||||
var asked []string
|
||||
call := func(key string, body any) (json.RawMessage, error) {
|
||||
asked = append(asked, key)
|
||||
return json.RawMessage(`{"seats":[{"seat":"other","holders":[{"module":"x"}]},{"seat":"router-seat","holders":[{"module":"holder"}]}]}`), nil
|
||||
}
|
||||
var bucket, id, subject string
|
||||
var published any
|
||||
var heard func(stdio.Envelope) error
|
||||
a := &Asker[warrant]{
|
||||
Module: "me", Contract: Contract{Seat: "router-seat", StateAsks: "asks"},
|
||||
Call: call,
|
||||
Record: func(b, i string) (*stdio.StateEntry, error) {
|
||||
bucket, id = b, i
|
||||
return &stdio.StateEntry{Value: json.RawMessage(`{"state":"chosen","warrant":{"ask":"a1","choice":"go"}}`)}, nil
|
||||
},
|
||||
Publish: func(s string, body any, _ string) error { subject, published = s, body; return nil },
|
||||
Subscribe: func(_ string, h func(stdio.Envelope) error) error { heard = h; return nil },
|
||||
AskSubject: func(m string) string { return "ask." + m },
|
||||
DecidedSubject: func(m string) string { return "decided." + m },
|
||||
}
|
||||
if err := a.Ask("a1", "q"); err != nil || subject != "ask.me" || published != "q" {
|
||||
t.Errorf("Ask published %v on %q: %v", published, subject, err)
|
||||
}
|
||||
state, w, err := a.Where("a1")
|
||||
if err != nil || state != "chosen" || w == nil || w.Choice != "go" || bucket != "holder_asks" || id != "a1" || len(asked) != 1 || asked[0] != "seat:mesh-module-assigner.assignments" {
|
||||
t.Errorf("Where = %q %+v %v, read %q %q after %v", state, w, err, bucket, id, asked)
|
||||
}
|
||||
var got []warrant
|
||||
if err := a.Hear(func(w warrant) error { got = append(got, w); return nil }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := heard(stdio.Envelope{Key: "decided.me", Body: []byte(`{"ask":"a1","choice":"go"}`)}); err != nil || len(got) != 1 || got[0].Ask != "a1" {
|
||||
t.Errorf("heard %v: %v", got, err)
|
||||
}
|
||||
if err := heard(stdio.Envelope{Key: "decided.me", Body: []byte(`nope`)}); err == nil {
|
||||
t.Errorf("a body that is no warrant was taken")
|
||||
}
|
||||
// No holder: nothing is read and the reason is said.
|
||||
a.Call = func(string, any) (json.RawMessage, error) { return json.RawMessage(`{"seats":[]}`), nil }
|
||||
if _, _, err := a.Where("a1"); err == nil || !strings.Contains(err.Error(), "no module holds the seat router-seat") {
|
||||
t.Errorf("with no holder Where answered %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user