From 043e3fb97959943b7d349b247a0606785fe0dde3 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 6 Nov 2026 22:39:31 +0100 Subject: [PATCH] Read seat definitions once: the indexer's reading becomes a module the checker requires (issue 1921) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The checking program's seat list parsed the definition format a second time beside the indexer's judge. The indexer's reading moves whole into its own sibling module seatfiles (a domain path, as takein), with ResolveRaw keeping every field as written for a request's seats; the judge and the take-in read through it, and the checker's rules only find the definitions and say why one is left out (the lead's ruling on D3, ADR 0634 §1). Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- modules/mesh-delivery-checker/go.mod | 4 + modules/mesh-delivery-checker/rules/seats.go | 68 ++----- modules/mesh-delivery-indexer/go.mod | 3 + .../internal/index/seatsof.go | 19 +- modules/mesh-delivery-indexer/judge/judge.go | 3 +- .../mesh-delivery-indexer/judge/judge_test.go | 5 +- .../mesh-delivery-indexer/judge/seatfiles.go | 126 +----------- .../mesh-delivery-indexer/seatfiles/go.mod | 5 + .../mesh-delivery-indexer/seatfiles/go.sum | 2 + .../seatfiles/seatfiles.go | 181 ++++++++++++++++++ 10 files changed, 228 insertions(+), 188 deletions(-) create mode 100644 modules/mesh-delivery-indexer/seatfiles/go.mod create mode 100644 modules/mesh-delivery-indexer/seatfiles/go.sum create mode 100644 modules/mesh-delivery-indexer/seatfiles/seatfiles.go diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index 8c2c7fe6..bbda335b 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -29,3 +29,7 @@ replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk + +require git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/seatfiles v0.0.0-00010101000000-000000000000 + +replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/seatfiles => ../mesh-delivery-indexer/seatfiles diff --git a/modules/mesh-delivery-checker/rules/seats.go b/modules/mesh-delivery-checker/rules/seats.go index 1ef80fcd..fda5aa3f 100644 --- a/modules/mesh-delivery-checker/rules/seats.go +++ b/modules/mesh-delivery-checker/rules/seats.go @@ -8,11 +8,15 @@ import ( "path" "slices" "strings" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/seatfiles" ) // The seat list a check makes from its heads (novox/hq ADR 0636 §4, ADR 0644 §1; the lead's ruling Q2 on milestone // 47/B0): the checking program built from the heads reads the seat definitions they carry and answers them resolved, so -// the running checker hands the manifest validator and the composer bytes it does not read itself. +// the running checker hands the manifest validator and the composer bytes it does not read itself. A definition is read +// by the indexer's one reading of the format (package seatfiles); this file finds the definitions and says why one is +// left out. // SeatsVersion is the version of SeatsAnswer this program writes. const SeatsVersion = 1 @@ -60,14 +64,12 @@ func SeatsOf(heads fs.FS) (SeatsAnswer, error) { } bySeat := map[string][]found{} for _, p := range files { - raw, err := resolvedDefinition(heads, p) + seat, raw, err := resolvedDefinition(heads, p) if err != nil { out.Left = append(out.Left, LeftOut{Path: p, Why: err.Error()}) continue } - var d definitionHead - _ = json.Unmarshal(raw, &d) - bySeat[d.Seat] = append(bySeat[d.Seat], found{p, raw}) + bySeat[seat] = append(bySeat[seat], found{p, raw}) } for _, seat := range slices.Sorted(maps.Keys(bySeat)) { fs := bySeat[seat] @@ -86,57 +88,21 @@ func SeatsOf(heads fs.FS) (SeatsAnswer, error) { return out, nil } -// definitionHead is what of a definition the list is made by: its seat, its scope and the parts it includes. The rest is -// carried as written, for the manifest validator and the composer to read (milestone G gives this the domain SDK's one -// reading). -type definitionHead struct { - Seat string `json:"seat"` - Scope string `json:"scope"` - Includes []string `json:"includes"` -} - -// resolvedDefinition is the definition at p with its parts' verbs and events put first and no includes, every field it -// carries kept as written. -func resolvedDefinition(heads fs.FS, p string) (json.RawMessage, error) { +// resolvedDefinition is the definition at p resolved with the parts it includes from the `parts` directory beside its +// `seats` directory, as the indexer's one reading of the format resolves it (seatfiles.ResolveRaw), every field kept as +// written. +func resolvedDefinition(heads fs.FS, p string) (string, json.RawMessage, error) { raw, err := fs.ReadFile(heads, p) if err != nil { - return nil, err + return "", nil, err } - var d definitionHead - if err := json.Unmarshal(raw, &d); err != nil { - return nil, fmt.Errorf("a seat definition is not JSON: %w", err) - } - if d.Seat == "" || (d.Scope != "mesh" && d.Scope != "node") { - return nil, fmt.Errorf("a seat definition names its seat and its scope (mesh or node), not %q and %q", d.Seat, d.Scope) - } - var whole map[string]json.RawMessage - if err := json.Unmarshal(raw, &whole); err != nil { - return nil, err - } - var verbs, events []json.RawMessage partsDir := path.Join(path.Dir(path.Dir(path.Dir(p))), "parts") - for _, name := range d.Includes { + d, out, err := seatfiles.ResolveRaw(raw, func(name string) ([]byte, error) { praw, err := fs.ReadFile(heads, path.Join(partsDir, name+".json")) if err != nil { - return nil, fmt.Errorf("the seat %s includes the part %q, which is not at %s", d.Seat, name, partsDir) + return nil, fmt.Errorf("it is not at %s", partsDir) } - var part struct{ Verbs, Events []json.RawMessage } - if err := json.Unmarshal(praw, &part); err != nil { - return nil, fmt.Errorf("the part %q of the seat %s is not JSON: %w", name, d.Seat, err) - } - verbs, events = append(verbs, part.Verbs...), append(events, part.Events...) - } - var own struct{ Verbs, Events []json.RawMessage } - _ = json.Unmarshal(raw, &own) - verbs, events = append(verbs, own.Verbs...), append(events, own.Events...) - delete(whole, "includes") - for k, list := range map[string][]json.RawMessage{"verbs": verbs, "events": events} { - if len(list) == 0 { - delete(whole, k) - continue - } - b, _ := json.Marshal(list) - whole[k] = b - } - return json.Marshal(whole) + return praw, nil + }) + return d.Seat, out, err } diff --git a/modules/mesh-delivery-indexer/go.mod b/modules/mesh-delivery-indexer/go.mod index 0f4eee43..35f43f26 100644 --- a/modules/mesh-delivery-indexer/go.mod +++ b/modules/mesh-delivery-indexer/go.mod @@ -26,6 +26,7 @@ replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun require ( git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/seatfiles v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 ) @@ -34,4 +35,6 @@ replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein => ./takein +replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/seatfiles => ./seatfiles + replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg => ../mesh-delivery-forge-bridge/forgemsg diff --git a/modules/mesh-delivery-indexer/internal/index/seatsof.go b/modules/mesh-delivery-indexer/internal/index/seatsof.go index 526e6e3b..78a2fb4a 100644 --- a/modules/mesh-delivery-indexer/internal/index/seatsof.go +++ b/modules/mesh-delivery-indexer/internal/index/seatsof.go @@ -18,9 +18,8 @@ import ( "slices" "strings" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/seatfiles" "git.novox.be/novox/mesh-sdk/go/catalogue" - - "mesh-delivery-indexer/judge" ) // definitionsDir is the directory of a contracts module's definitions that the module.json at dir names, from the first @@ -58,7 +57,7 @@ func (ix *Indexer) revisionSeats(ctx context.Context, ready []PendingEntry) ([]c if ix.ReadFile == nil || ix.Files == nil { return nil, nil } - var defs []judge.Definition + var defs []seatfiles.Definition for _, e := range ready { if e.Covered || e.Forge.Merge == "" { continue @@ -75,7 +74,7 @@ func (ix *Indexer) revisionSeats(ctx context.Context, ready []PendingEntry) ([]c defs = append(defs, got...) } } - seats, err := judge.Seats(defs) + seats, err := seatfiles.Seats(defs) if err != nil { return nil, err } @@ -84,7 +83,7 @@ func (ix *Indexer) revisionSeats(ctx context.Context, ready []PendingEntry) ([]c // readDefinitions reads the definitions under a contracts module's definitions directory at a commit, resolved with their // parts. -func (ix *Indexer) readDefinitions(ctx context.Context, repository, commit, defsDir string) ([]judge.Definition, error) { +func (ix *Indexer) readDefinitions(ctx context.Context, repository, commit, defsDir string) ([]seatfiles.Definition, error) { file := func(p string) ([]byte, error) { if defsDir != "." && defsDir != "" { p = defsDir + "/" + p @@ -103,8 +102,8 @@ func (ix *Indexer) readDefinitions(ctx context.Context, repository, commit, defs if err != nil { return nil, fmt.Errorf("listing %s of %s at %.12s: %w", dir, repository, commit, err) } - parts := map[string]judge.Part{} - var out []judge.Definition + parts := map[string]seatfiles.Part{} + var out []seatfiles.Definition for _, p := range slices.Sorted(maps.Keys(listed[dir])) { seat, ok := strings.CutSuffix(p, "/seat.json") if !ok || seat == "" || strings.Contains(seat, "/") { @@ -114,7 +113,7 @@ func (ix *Indexer) readDefinitions(ctx context.Context, repository, commit, defs if err != nil { return nil, err } - d, err := judge.ParseDefinition(raw) + d, err := seatfiles.ParseDefinition(raw) if err == nil && d.Seat != seat { err = fmt.Errorf("it names the seat %q, not %q", d.Seat, seat) } @@ -129,11 +128,11 @@ func (ix *Indexer) readDefinitions(ctx context.Context, repository, commit, defs if perr != nil { return nil, perr } - var part judge.Part + var part seatfiles.Part if praw == nil { perr = errors.New("the commit holds no such part") } else { - part, perr = judge.ParsePart(praw) + part, perr = seatfiles.ParsePart(praw) } if perr != nil { err = fmt.Errorf("its part %s: %w", name, perr) diff --git a/modules/mesh-delivery-indexer/judge/judge.go b/modules/mesh-delivery-indexer/judge/judge.go index c144cf02..9f39b3ae 100644 --- a/modules/mesh-delivery-indexer/judge/judge.go +++ b/modules/mesh-delivery-indexer/judge/judge.go @@ -25,6 +25,7 @@ import ( "strings" "sync" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/seatfiles" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/composer" ) @@ -212,7 +213,7 @@ func Request(r composer.Request) (composer.Verdict, error) { return RequestWith( // the members' heads' definitions, so a manifest that serves or invokes what the group's definitions add is judged by // them. None is Request. func RequestWith(r composer.Request, seats [][]byte) (composer.Verdict, error) { - over, err := SeatsOfFiles(seats) + over, err := seatfiles.SeatsOfFiles(seats) if err != nil { return composer.Verdict{}, fmt.Errorf("a seat definition of the request: %w", err) } diff --git a/modules/mesh-delivery-indexer/judge/judge_test.go b/modules/mesh-delivery-indexer/judge/judge_test.go index b57cfe4d..629fbcce 100644 --- a/modules/mesh-delivery-indexer/judge/judge_test.go +++ b/modules/mesh-delivery-indexer/judge/judge_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/seatfiles" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/composer" "git.novox.be/novox/mesh-sdk/go/overlay" @@ -155,8 +156,8 @@ func TestTheJudge(t *testing.T) { t.Fatalf("%s: the seats are not put back", row.name) } } - parts := map[string]Part{"reads": {Verbs: []DefVerb{{Name: "manifest-reads"}}}} - d, _ := ParseDefinition(defs[0]) + parts := map[string]seatfiles.Part{"reads": {Verbs: []seatfiles.DefVerb{{Name: "manifest-reads"}}}} + d, _ := seatfiles.ParseDefinition(defs[0]) if d, err := d.Resolve(parts); err != nil || len(d.Verbs) != 2 || d.Verbs[0].Name != "manifest-reads" { t.Errorf("a resolved definition: %+v %v", d, err) } diff --git a/modules/mesh-delivery-indexer/judge/seatfiles.go b/modules/mesh-delivery-indexer/judge/seatfiles.go index 384baace..b1921955 100644 --- a/modules/mesh-delivery-indexer/judge/seatfiles.go +++ b/modules/mesh-delivery-indexer/judge/seatfiles.go @@ -1,137 +1,15 @@ package judge -// The seat definitions a revision carries (novox/hq ADR 0633 §1, §6; ADR 0635, ADR 0636 §4): a manifest is judged -// against the seats of the revision it belongs to, never against the list this program was built with. A definition is -// the contracts module's `seats//seat.json` (design 26 *A seat's definition*), with the parts it includes from -// `parts/.json`. This reader takes of it only what a manifest is judged by (the seat's scope, what it delivers, -// its verbs and events) and reads the rest leniently: the format's one strict reader (mesh-sdk seatdef) is not in the -// mesh-sdk this judge is built with, and the indexer of the revision keeps the definitions whole. +// The seat list a judge pass judges with: the seats a request or a revision carries (read by the indexer's one reading, +// package seatfiles) put over the catalogue's list for that pass. import ( - "encoding/json" - "fmt" "slices" "sync" "git.novox.be/novox/mesh-sdk/go/catalogue" ) -// Definition is the part of one seat's definition a manifest is judged by. -type Definition struct { - Format int `json:"format"` - Seat string `json:"seat"` - Scope string `json:"scope"` - Decision string `json:"decision"` - HolderDeclares bool `json:"holder-declares"` - Delivers string `json:"delivers"` - Replicated bool `json:"replicated"` - PulledByKind bool `json:"pulled-by-kind"` - Yields bool `json:"yields"` - HeldByTheEngine bool `json:"held-by-the-engine"` - Includes []string `json:"includes"` - Verbs []DefVerb `json:"verbs"` - Accepts []DefName `json:"accepts"` - Events []DefName `json:"events"` -} - -// DefVerb is a verb of a definition. -type DefVerb struct { - Name string `json:"name"` - Description string `json:"description"` - Optional bool `json:"optional"` - Replaces []string `json:"replaces"` -} - -// DefName is an accepted message or an event of a definition. -type DefName struct { - Name string `json:"name"` -} - -// Part is a part a definition includes: verbs and events written once for the seats that share them. -type Part struct { - Part string `json:"part"` - Verbs []DefVerb `json:"verbs"` - Events []DefName `json:"events"` -} - -// ParseDefinition reads one definition; a file that is not one names a seat and a scope. -func ParseDefinition(raw []byte) (Definition, error) { - var d Definition - if err := json.Unmarshal(raw, &d); err != nil { - return Definition{}, fmt.Errorf("a seat definition is not JSON: %w", err) - } - if d.Seat == "" || (d.Scope != catalogue.ScopeMesh && d.Scope != catalogue.ScopeNode) { - return Definition{}, fmt.Errorf("a seat definition names its seat and its scope (mesh or node), not %q and %q", d.Seat, d.Scope) - } - return d, nil -} - -// ParsePart reads one part. -func ParsePart(raw []byte) (Part, error) { - var p Part - if err := json.Unmarshal(raw, &p); err != nil { - return Part{}, fmt.Errorf("a part of a seat definition is not JSON: %w", err) - } - return p, nil -} - -// Resolve is the definition with the verbs and events of the parts it includes put first, as the indexer keeps it. -func (d Definition) Resolve(parts map[string]Part) (Definition, error) { - out := d - out.Includes, out.Verbs, out.Events = nil, nil, nil - for _, name := range d.Includes { - p, ok := parts[name] - if !ok { - return Definition{}, fmt.Errorf("the seat %s includes the part %q, which is not given", d.Seat, name) - } - out.Verbs = append(out.Verbs, p.Verbs...) - out.Events = append(out.Events, p.Events...) - } - out.Verbs = append(out.Verbs, d.Verbs...) - out.Events = append(out.Events, d.Events...) - return out, nil -} - -// Seats are the seats of resolved definitions, in the catalogue's types; a seat its holder declares itself is left out. -// A definition still naming parts it includes is refused: resolve it first. -func Seats(defs []Definition) ([]catalogue.Seat, error) { - var out []catalogue.Seat - for _, d := range defs { - if d.HolderDeclares { - continue - } - if len(d.Includes) > 0 { - return nil, fmt.Errorf("the seat %s names parts it includes: resolve it first", d.Seat) - } - s := catalogue.Seat{Name: d.Seat, Scope: d.Scope, Delivers: d.Delivers, Replicated: d.Replicated, - PulledByKind: d.PulledByKind, Yields: d.Yields, HeldByTheEngine: d.HeldByTheEngine, Decision: d.Decision} - for _, a := range d.Accepts { - s.Accepts = append(s.Accepts, a.Name) - } - for _, e := range d.Events { - s.Emits = append(s.Emits, e.Name) - } - for _, v := range d.Verbs { - s.Serves = append(s.Serves, catalogue.Verb{Name: v.Name, Description: v.Description, Optional: v.Optional, Replaces: v.Replaces}) - } - out = append(out, s) - } - return out, nil -} - -// SeatsOfFiles are the seats of resolved definitions given as files, as a request carries them. -func SeatsOfFiles(raws [][]byte) ([]catalogue.Seat, error) { - defs := make([]Definition, 0, len(raws)) - for _, raw := range raws { - d, err := ParseDefinition(raw) - if err != nil { - return nil, err - } - defs = append(defs, d) - } - return Seats(defs) -} - // merged is base with each seat of over put in place of the seat of its name, or added: a definition is a seat's whole // contract. A verb the base seat holds keeps its input and output (a definition read here carries none). func merged(base, over []catalogue.Seat) []catalogue.Seat { diff --git a/modules/mesh-delivery-indexer/seatfiles/go.mod b/modules/mesh-delivery-indexer/seatfiles/go.mod new file mode 100644 index 00000000..4fc2f656 --- /dev/null +++ b/modules/mesh-delivery-indexer/seatfiles/go.mod @@ -0,0 +1,5 @@ +module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/seatfiles + +go 1.26.0 + +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 diff --git a/modules/mesh-delivery-indexer/seatfiles/go.sum b/modules/mesh-delivery-indexer/seatfiles/go.sum new file mode 100644 index 00000000..60d9d4a6 --- /dev/null +++ b/modules/mesh-delivery-indexer/seatfiles/go.sum @@ -0,0 +1,2 @@ +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 h1:tK7MzxVm/ECNg1Yblhh1ynVEthbZCg2mSPW0wQZC0b0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= diff --git a/modules/mesh-delivery-indexer/seatfiles/seatfiles.go b/modules/mesh-delivery-indexer/seatfiles/seatfiles.go new file mode 100644 index 00000000..f28adb67 --- /dev/null +++ b/modules/mesh-delivery-indexer/seatfiles/seatfiles.go @@ -0,0 +1,181 @@ +// Package seatfiles is the indexer's one reading of the seat definitions a revision carries (novox/hq ADR 0633 §1, §6; +// ADR 0635, ADR 0636 §4): a manifest is judged against the seats of the revision it belongs to, never against the list +// the judge was built with. A definition is the contracts module's `seats//seat.json` (design 26 *A seat's +// definition*), with the parts it includes from `parts/.json`. This reader takes of it only what a manifest is +// judged by (the seat's scope, what it delivers, its verbs and events) and reads the rest leniently: the format's one +// strict reader (mesh-sdk seatdef) is not in the mesh-sdk the judge is built with, and the indexer of the revision keeps +// the definitions whole. +// +// It is the indexer's own package, a module of its own so that the checker's rules can require it (the lead's ruling on +// milestone 47/B0, ADR 0634 §1: shared code lives with its owner): the indexer's judge and the checker's seat list read +// definitions here and nowhere else. Milestone G's domain SDK reading replaces it. +package seatfiles + +import ( + "encoding/json" + "fmt" + + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// Definition is the part of one seat's definition a manifest is judged by. +type Definition struct { + Format int `json:"format"` + Seat string `json:"seat"` + Scope string `json:"scope"` + Decision string `json:"decision"` + HolderDeclares bool `json:"holder-declares"` + Delivers string `json:"delivers"` + Replicated bool `json:"replicated"` + PulledByKind bool `json:"pulled-by-kind"` + Yields bool `json:"yields"` + HeldByTheEngine bool `json:"held-by-the-engine"` + Includes []string `json:"includes"` + Verbs []DefVerb `json:"verbs"` + Accepts []DefName `json:"accepts"` + Events []DefName `json:"events"` +} + +// DefVerb is a verb of a definition. +type DefVerb struct { + Name string `json:"name"` + Description string `json:"description"` + Optional bool `json:"optional"` + Replaces []string `json:"replaces"` +} + +// DefName is an accepted message or an event of a definition. +type DefName struct { + Name string `json:"name"` +} + +// Part is a part a definition includes: verbs and events written once for the seats that share them. +type Part struct { + Part string `json:"part"` + Verbs []DefVerb `json:"verbs"` + Events []DefName `json:"events"` +} + +// ParseDefinition reads one definition; a file that is not one names a seat and a scope. +func ParseDefinition(raw []byte) (Definition, error) { + var d Definition + if err := json.Unmarshal(raw, &d); err != nil { + return Definition{}, fmt.Errorf("a seat definition is not JSON: %w", err) + } + if d.Seat == "" || (d.Scope != catalogue.ScopeMesh && d.Scope != catalogue.ScopeNode) { + return Definition{}, fmt.Errorf("a seat definition names its seat and its scope (mesh or node), not %q and %q", d.Seat, d.Scope) + } + return d, nil +} + +// ParsePart reads one part. +func ParsePart(raw []byte) (Part, error) { + var p Part + if err := json.Unmarshal(raw, &p); err != nil { + return Part{}, fmt.Errorf("a part of a seat definition is not JSON: %w", err) + } + return p, nil +} + +// Resolve is the definition with the verbs and events of the parts it includes put first, as the indexer keeps it. +func (d Definition) Resolve(parts map[string]Part) (Definition, error) { + out := d + out.Includes, out.Verbs, out.Events = nil, nil, nil + for _, name := range d.Includes { + p, ok := parts[name] + if !ok { + return Definition{}, fmt.Errorf("the seat %s includes the part %q, which is not given", d.Seat, name) + } + out.Verbs = append(out.Verbs, p.Verbs...) + out.Events = append(out.Events, p.Events...) + } + out.Verbs = append(out.Verbs, d.Verbs...) + out.Events = append(out.Events, d.Events...) + return out, nil +} + +// Seats are the seats of resolved definitions, in the catalogue's types; a seat its holder declares itself is left out. +// A definition still naming parts it includes is refused: resolve it first. +func Seats(defs []Definition) ([]catalogue.Seat, error) { + var out []catalogue.Seat + for _, d := range defs { + if d.HolderDeclares { + continue + } + if len(d.Includes) > 0 { + return nil, fmt.Errorf("the seat %s names parts it includes: resolve it first", d.Seat) + } + s := catalogue.Seat{Name: d.Seat, Scope: d.Scope, Delivers: d.Delivers, Replicated: d.Replicated, + PulledByKind: d.PulledByKind, Yields: d.Yields, HeldByTheEngine: d.HeldByTheEngine, Decision: d.Decision} + for _, a := range d.Accepts { + s.Accepts = append(s.Accepts, a.Name) + } + for _, e := range d.Events { + s.Emits = append(s.Emits, e.Name) + } + for _, v := range d.Verbs { + s.Serves = append(s.Serves, catalogue.Verb{Name: v.Name, Description: v.Description, Optional: v.Optional, Replaces: v.Replaces}) + } + out = append(out, s) + } + return out, nil +} + +// SeatsOfFiles are the seats of resolved definitions given as files, as a request carries them. +func SeatsOfFiles(raws [][]byte) ([]catalogue.Seat, error) { + defs := make([]Definition, 0, len(raws)) + for _, raw := range raws { + d, err := ParseDefinition(raw) + if err != nil { + return nil, err + } + defs = append(defs, d) + } + return Seats(defs) +} + +// ResolveRaw is the definition raw resolved with the parts it includes, each read by part, every field it carries kept as +// written: the verbs and events of its parts put first, then its own, and no includes. It reads raw and every part as +// ParseDefinition and ParsePart do, and answers the definition read with it. It is how a request's `seats` carry a +// definition whole (ADR 0636 §4). +func ResolveRaw(raw []byte, part func(name string) ([]byte, error)) (Definition, json.RawMessage, error) { + d, err := ParseDefinition(raw) + if err != nil { + return Definition{}, nil, err + } + var whole map[string]json.RawMessage + if err := json.Unmarshal(raw, &whole); err != nil { + return Definition{}, nil, err + } + type listed struct{ Verbs, Events []json.RawMessage } + var verbs, events []json.RawMessage + for _, name := range d.Includes { + praw, err := part(name) + if err != nil { + return Definition{}, nil, fmt.Errorf("the seat %s includes the part %q: %w", d.Seat, name, err) + } + var l listed + if _, err := ParsePart(praw); err != nil { + return Definition{}, nil, fmt.Errorf("the part %q of the seat %s: %w", name, d.Seat, err) + } + _ = json.Unmarshal(praw, &l) + verbs, events = append(verbs, l.Verbs...), append(events, l.Events...) + } + var own listed + _ = json.Unmarshal(raw, &own) + verbs, events = append(verbs, own.Verbs...), append(events, own.Events...) + delete(whole, "includes") + for k, list := range map[string][]json.RawMessage{"verbs": verbs, "events": events} { + if len(list) == 0 { + delete(whole, k) + continue + } + b, err := json.Marshal(list) + if err != nil { + return Definition{}, nil, err + } + whole[k] = b + } + out, err := json.Marshal(whole) + return d, out, err +}