diff --git a/modules/mesh-delivery-indexer/judge/busstate.go b/modules/mesh-delivery-indexer/judge/busstate.go index 843849e6..46598667 100644 --- a/modules/mesh-delivery-indexer/judge/busstate.go +++ b/modules/mesh-delivery-indexer/judge/busstate.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "maps" "slices" "strings" @@ -347,7 +348,9 @@ func workQueueProblems(at string, e catalogue.BusStateEntry) []string { return nil } -// ledgerTypeProblems judges a ledger's types: named locally and once, and every operation's effect one of the four. +// ledgerTypeProblems judges a ledger's types: named locally and once, and every operation's effect one of the four; a +// set-entry or remove names its `into` and `key`; an operation's `fields` and an index's `on` name fields of the type's +// schema; a `digest-of` names a text field of it (DLV-I12, ADR 0595 §8, ADR 0597 §2, §4). func ledgerTypeProblems(at string, e catalogue.BusStateEntry) []string { var problems []string if len(e.Types) == 0 { @@ -374,6 +377,34 @@ func ledgerTypeProblems(at string, e catalogue.BusStateEntry) []string { "%s: the operation %q has the effect %q, outside the four (%s) (novox/hq ADR 0597 §2)", where, op.Name, op.Effect, strings.Join(ledgerEffects, ", "))) } + if op.Effect == catalogue.EffectSetEntry || op.Effect == catalogue.EffectRemove { + for field, v := range map[string]string{"into": op.Into, "key": op.Key} { + if strings.TrimSpace(v) == "" { + problems = append(problems, fmt.Sprintf("%s: the operation %q (%s) names no %s (novox/hq ADR 0597 §2)", where, op.Name, op.Effect, field)) + } + } + } + for _, f := range op.Fields { + if _, ok := t.Schema[f]; !ok { + problems = append(problems, fmt.Sprintf("%s: the operation %q names the field %q, which the type's schema does not hold", where, op.Name, f)) + } + } + } + for _, ix := range t.Indexes { + for _, f := range ix.On { + if _, ok := t.Schema[f]; !ok { + problems = append(problems, fmt.Sprintf("%s: the index %q is on the field %q, which the type's schema does not hold (novox/hq ADR 0597 §4)", where, ix.Name, f)) + } + } + } + for _, name := range slices.Sorted(maps.Keys(t.Schema)) { + of := t.Schema[name].DigestOf + if of == "" { + continue + } + if src, ok := t.Schema[of]; !ok || src.Type != "text" { + problems = append(problems, fmt.Sprintf("%s: the field %q is the digest of %q, which is no text field of the type's schema", where, name, of)) + } } } return problems diff --git a/modules/mesh-delivery-indexer/judge/busstate_test.go b/modules/mesh-delivery-indexer/judge/busstate_test.go index f2d8f751..31255dde 100644 --- a/modules/mesh-delivery-indexer/judge/busstate_test.go +++ b/modules/mesh-delivery-indexer/judge/busstate_test.go @@ -52,6 +52,19 @@ func TestBusStateIsJudgedOnce(t *testing.T) { {"a bare stream with a wrong when full", manifest(`{"name":"x","storage":"stream","configuration":{"when-full":"crash","max-mib":10,"max-age-days":9}}`), []string{"\"x\"", "\"when-full\"", "\"refuse\" or \"drop-oldest\""}}, {"a ledger effect outside the four", manifest(strings.Replace(ledger, `"set-fields"`, `"upsert"`, 1)), []string{"\"records\"", "effect \"upsert\"", "outside the four"}}, {"a ledger whose history expires", manifest(strings.Replace(ledger, `"options"`, `"configuration":{"max-age-days":30},"options"`, 1)), nil}, + // DLV-I12 (ADR 0595 §8, ADR 0597 §2, §4): each refusal names the module, the entry, the type, the operation or index, and the field. + {"a set-entry naming no into", manifest(strings.Replace(ledger, `"effect":"set-fields","fields":["title"]`, `"effect":"set-entry","key":"title"`, 1)), + []string{"\"records\"", "type \"decision\"", "operation \"propose\"", "no into"}}, + {"a remove naming no key", manifest(strings.Replace(ledger, `"effect":"set-fields","fields":["title"]`, `"effect":"remove","into":"links"`, 1)), + []string{"\"records\"", "operation \"propose\"", "no key"}}, + {"a set-entry naming both", manifest(strings.Replace(ledger, `"effect":"set-fields","fields":["title"]`, `"effect":"set-entry","into":"links","key":"title","fields":["title"]`, 1)), nil}, + {"an operation's field the schema lacks", manifest(strings.Replace(ledger, `"fields":["title"]`, `"fields":["title","body"]`, 1)), + []string{"\"records\"", "operation \"propose\"", "field \"body\"", "schema does not hold"}}, + {"an index on a field the schema lacks", manifest(strings.Replace(ledger, `"fields":["title"]}]`, `"fields":["title"]}],"indexes":[{"name":"by-owner","on":["owner"]}]`, 1)), + []string{"\"records\"", "index \"by-owner\"", "field \"owner\""}}, + {"a digest of a field that is no text", manifest(strings.Replace(ledger, `{"title":{"type":"text"}}`, `{"title":{"type":"text"},"n":{"type":"integer"},"sum":{"type":"text","digest-of":"n"}}`, 1)), + []string{"\"records\"", "field \"sum\"", "digest of \"n\"", "no text field"}}, + {"a digest of a text field", manifest(strings.Replace(ledger, `{"title":{"type":"text"}}`, `{"title":{"type":"text"},"sum":{"type":"text","digest-of":"title"}}`, 1)), nil}, } { _, problems := Manifest([]byte(c.raw), nil) switch {