diff --git a/modules/nats/cmd/nats-tools/catalogview.go b/modules/nats/cmd/nats-tools/catalogview.go new file mode 100644 index 000000000..ee8fb77fe --- /dev/null +++ b/modules/nats/cmd/nats-tools/catalogview.go @@ -0,0 +1,57 @@ +package main + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + + catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-sdk/go/catalogue" + + "nats-tools/internal/keeper" +) + +// The keeper reads a module's asks from the module's entry in the catalog half of its revision view, not from the +// indexer's event (design 47 G §344, §433). + +// Asks is what one catalog graph entry says of a module for the keeper: what its manifest asks of the bus as an accepted +// verdict, the seats it claims, and whether it declares an event of its own (the facts keeper.Sends reads). +type Asks struct { + Accepted keeper.Accepted + Claims []string + EmitsOwn bool +} + +// AsksOf reads one entry. An entry whose manifest is not accepted asks nothing of the bus now, as an accepted verdict of +// no entries does; the digest it carries is of the entries it asks, so an entry changed in nothing the bus module reads +// is not applied again. +func AsksOf(e catalog.CatalogEntry) (Asks, error) { + if e.Module == "" { + return Asks{}, fmt.Errorf("a catalog entry names no module") + } + raw, err := json.Marshal(e.Manifest) + if err != nil { + return Asks{}, fmt.Errorf("the manifest of %s is not readable: %w", e.Module, err) + } + var m catalogue.Manifest + if err := json.Unmarshal(raw, &m); err != nil { + return Asks{}, fmt.Errorf("the manifest of %s is not readable: %w", e.Module, err) + } + state := m.BusState + if e.Verdict != "" && e.Verdict != "accepted" { + state = nil + } + sum := sha256.Sum256(mustJSON(state)) + out := Asks{Accepted: keeper.Accepted{Module: e.Module, Digest: hex.EncodeToString(sum[:]), BusState: state}, + EmitsOwn: len(m.Emits) > 0 || len(e.Streams) > 0} + for _, c := range m.Claims { + out.Claims = append(out.Claims, c.Name) + } + return out, nil +} + +func mustJSON(v any) []byte { + b, _ := json.Marshal(v) + return b +} diff --git a/modules/nats/cmd/nats-tools/catalogview_test.go b/modules/nats/cmd/nats-tools/catalogview_test.go new file mode 100644 index 000000000..b49659993 --- /dev/null +++ b/modules/nats/cmd/nats-tools/catalogview_test.go @@ -0,0 +1,28 @@ +package main + +import ( + "testing" + + catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" +) + +func TestAsksOf(t *testing.T) { + m := map[string]any{"module": "m", "claims": []any{map[string]any{"name": "seat-a"}}, "emits": []any{"done"}} + a, err := AsksOf(catalog.CatalogEntry{Module: "m", Verdict: "accepted", Manifest: m}) + if err != nil { + t.Fatal(err) + } + if len(a.Claims) != 1 || a.Claims[0] != "seat-a" || !a.EmitsOwn || a.Accepted.Module != "m" || a.Accepted.Digest == "" { + t.Errorf("read %+v", a) + } + b, _ := AsksOf(catalog.CatalogEntry{Module: "m", Manifest: map[string]any{"module": "m"}}) + if b.EmitsOwn || len(b.Claims) != 0 { + t.Errorf("an entry with neither: %+v", b) + } + if b.Accepted.Digest != a.Accepted.Digest { + t.Errorf("the digest is of the bus-state asked alone: %s vs %s", b.Accepted.Digest, a.Accepted.Digest) + } + if _, err := AsksOf(catalog.CatalogEntry{}); err == nil { + t.Error("an entry with no module was read") + } +} diff --git a/modules/nats/internal/keeper/keeper_test.go b/modules/nats/internal/keeper/keeper_test.go index 735ed9a4d..ef9af998f 100644 --- a/modules/nats/internal/keeper/keeper_test.go +++ b/modules/nats/internal/keeper/keeper_test.go @@ -14,6 +14,7 @@ import ( occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/seatdef" ) // fakeBus holds objects by stream, as a bus would, and refuses what a test tells it to. @@ -794,3 +795,31 @@ func TestTheKeeperMakesWhatAVerdictAsksForAndSaysIt(t *testing.T) { }) } } + +func TestSends(t *testing.T) { + defs := map[string]seatdef.Definition{ + "verbed": {Verbs: []seatdef.Verb{{Name: "go"}}}, + "evented": {Events: []seatdef.Message{{Name: "done"}}}, + "included": {Includes: []string{"part"}}, + "bare": {}, + } + for _, c := range []struct { + name string + claims []string + own bool + want bool + }{ + {"a seat with a verb", []string{"verbed"}, false, true}, + {"a seat with an event", []string{"evented"}, false, true}, + {"a seat that includes parts", []string{"included"}, false, true}, + {"a seat with neither", []string{"bare"}, false, false}, + {"a seat the list does not hold", []string{"unknown"}, false, false}, + {"an event of its own", nil, true, true}, + {"nothing claimed", nil, false, false}, + {"one of two claims sends", []string{"bare", "verbed"}, false, true}, + } { + if got := Sends(c.claims, c.own, defs); got != c.want { + t.Errorf("%s: Sends = %v, want %v", c.name, got, c.want) + } + } +} diff --git a/modules/nats/internal/keeper/plan.go b/modules/nats/internal/keeper/plan.go index 080d61755..ad89ae4e9 100644 --- a/modules/nats/internal/keeper/plan.go +++ b/modules/nats/internal/keeper/plan.go @@ -16,6 +16,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/membership" + "git.novox.be/novox/mesh-sdk/go/seatdef" ) // The kinds of bus object an entry is made of: a stream, a key-value bucket (stream KV_) and an object store @@ -48,6 +49,22 @@ const ( SentAgeDays = 7 ) +// Sends says whether a module sends (design 47 G §344): it claims a seat whose definition has a verb or an event, or it +// declares an event of its own (emitsOwn). claims are the seats its catalog graph entry claims, defs the seat list's +// definitions by seat; a claimed seat the list does not hold counts for nothing. A definition that includes parts takes +// their verbs and events whole (seatdef.Resolve), so one counts as having some. +func Sends(claims []string, emitsOwn bool, defs map[string]seatdef.Definition) bool { + if emitsOwn { + return true + } + for _, c := range claims { + if d, ok := defs[c]; ok && (len(d.Verbs) > 0 || len(d.Events) > 0 || len(d.Includes) > 0) { + return true + } + } + return false +} + // Settings are one bus object's settings, as asked and as held: what check compares. type Settings struct { // MaxBytes is its cap in bytes; MaxAgeSeconds its age, 0 for none.