diff --git a/go/catalogue/manifest.go b/go/catalogue/manifest.go index 9c7274b..5d501f9 100644 --- a/go/catalogue/manifest.go +++ b/go/catalogue/manifest.go @@ -465,6 +465,11 @@ type Manifest struct { // choosing `kc` for keycloak keeps the identity legible where a hash would not. Slug string `json:"slug,omitempty"` + // NeedsApproval marks tools the module serves (``), seat verbs it serves (`.`) and events it + // emits that need the operator's approval first (design 46 *The approval mark* §1; design 34 every-event §32). Absent + // is no mark. + NeedsApproval []string `json:"needs-approval,omitempty"` + // Provides are the names other modules may require. A module always provides its own name; // this is for the rest — `zsh` provides `shell`, `xorg` provides `display-server`. Provides []Offer `json:"provides,omitempty"` diff --git a/go/catalogue/reader_release_test.go b/go/catalogue/reader_release_test.go index d81091c..43cdf35 100644 --- a/go/catalogue/reader_release_test.go +++ b/go/catalogue/reader_release_test.go @@ -6,13 +6,16 @@ import "testing" // `version` as MAJOR.MINOR (§368), a `*` emitter in `consumes` (§386), an observer's finding stream (§387) and an // artifact's `needs` (§47, §432). func TestTheReaderReleaseReadsTheManifestWordsGWrites(t *testing.T) { - m, err := decodeForTest([]byte(`{"module":"mesh-watcher","version":"1.4",` + + m, err := decodeForTest([]byte(`{"module":"mesh-watcher","version":"1.4","needs-approval":["restart","node-engine.composition","finding-written"],` + `"consumes":["*.finding-written","mesh-conditions.findings-asked"],` + `"streams":[{"name":"mesh-watcher-findings","events":["finding-written","finding-cleared","findings-stated"],"max-age-days":7,"max-mib":16}],` + `"build":{"artifacts":[{"name":"program","kind":"bundle","language":"go","needs":["mesh-grants:program","node-tools:runtime"]}]}}`)) if err != nil { t.Fatalf("a manifest with G's words was refused: %v", err) } + if len(m.UnknownField()) != 0 || len(m.NeedsApproval) != 3 { + t.Fatalf("a word was set aside: %v (needs-approval %v)", m.UnknownField(), m.NeedsApproval) + } if m.Version != "1.4" || m.Consumes[0] != "*.finding-written" || m.Streams[0].Name != "mesh-watcher-findings" || len(m.Build.Artifacts[0].Needs) != 2 || m.Build.Artifacts[0].Needs[0] != "mesh-grants:program" { t.Fatalf("read as %+v", m) diff --git a/go/seatdef/refs.go b/go/seatdef/refs.go index 435005a..202f087 100644 --- a/go/seatdef/refs.go +++ b/go/seatdef/refs.go @@ -1,18 +1,28 @@ package seatdef -import "strings" - // ResolveRefs is a schema (its all-string lists as []string, StringLists) with every `{"$ref": "#/defs/"}` replaced by the definition's named type, as the seat // list's readers need it (a verb's input and answer are one schema each, not a schema and its types). A reference to a // name the definition does not define stays as it is; nesting deeper than 32 levels stops there. -func ResolveRefs(schema map[string]any, defs map[string]map[string]any) map[string]any { +// +// A reference `#/bodies/` resolves against the definition's bodies, given after defs (design 47 G §439). +func ResolveRefs(schema map[string]any, defs map[string]map[string]any, bodies ...map[string]map[string]any) map[string]any { if schema == nil { return nil } - m, _ := StringLists(resolveValue(schema, defs, 0)).(map[string]any) + all := make(map[string]map[string]any, len(defs)) + for n, d := range defs { + all["#/defs/"+n] = d + } + for _, b := range bodies { + for n, d := range b { + all["#/bodies/"+n] = d + } + } + m, _ := StringLists(resolveValue(schema, all, 0)).(map[string]any) return m } +// resolveValue resolves against refs, each reference as written (`#/defs/`, `#/bodies/`). func resolveValue(v any, defs map[string]map[string]any, depth int) any { if depth > 32 { return v @@ -20,7 +30,7 @@ func resolveValue(v any, defs map[string]map[string]any, depth int) any { switch x := v.(type) { case map[string]any: if r, ok := x["$ref"].(string); ok { - if d, ok := defs[strings.TrimPrefix(r, "#/defs/")]; ok { + if d, ok := defs[r]; ok { return resolveValue(d, defs, depth+1) } } diff --git a/go/seatdef/refs_test.go b/go/seatdef/refs_test.go index 59576a4..9358da6 100644 --- a/go/seatdef/refs_test.go +++ b/go/seatdef/refs_test.go @@ -11,6 +11,7 @@ func TestResolveRefsPutsNamedTypesInPlace(t *testing.T) { "id": {"type": "string"}, "row": {"type": "object", "properties": map[string]any{"id": map[string]any{"$ref": "#/defs/id"}}}, } + bodies := map[string]map[string]any{"Finding": {"type": "object", "properties": map[string]any{"id": map[string]any{"$ref": "#/defs/id"}}}} cases := []struct { name string schema, want map[string]any @@ -20,11 +21,15 @@ func TestResolveRefsPutsNamedTypesInPlace(t *testing.T) { map[string]any{"type": "array", "items": map[string]any{"type": "object", "properties": map[string]any{"id": map[string]any{"type": "string"}}}}}, {"a name it does not define stays", map[string]any{"$ref": "#/defs/gone"}, map[string]any{"$ref": "#/defs/gone"}}, + {"a body", map[string]any{"type": "array", "items": map[string]any{"$ref": "#/bodies/Finding"}}, + map[string]any{"type": "array", "items": map[string]any{"type": "object", + "properties": map[string]any{"id": map[string]any{"type": "string"}}}}}, + {"a body it does not define stays", map[string]any{"$ref": "#/bodies/Gone"}, map[string]any{"$ref": "#/bodies/Gone"}}, {"a list of schemas", map[string]any{"oneOf": []any{map[string]any{"$ref": "#/defs/id"}}}, map[string]any{"oneOf": []any{map[string]any{"type": "string"}}}}, } for _, c := range cases { - if got := ResolveRefs(c.schema, defs); !reflect.DeepEqual(got, c.want) { + if got := ResolveRefs(c.schema, defs, bodies); !reflect.DeepEqual(got, c.want) { t.Errorf("%s: %v, want %v", c.name, got, c.want) } } diff --git a/go/seatdef/seatdef.go b/go/seatdef/seatdef.go index 410fe40..7b09c9b 100644 --- a/go/seatdef/seatdef.go +++ b/go/seatdef/seatdef.go @@ -93,6 +93,9 @@ type Definition struct { Paths []Path `json:"paths,omitempty"` // Defs are named types a schema refers to as `{"$ref": "#/defs/"}`. Defs map[string]map[string]any `json:"defs,omitempty"` + // Bodies are the bodies other modules say on their own streams, named: each a JSON Schema, referred to as + // `{"$ref": "#/bodies/"}`. The grants part makes no seat event or stream from them (design 47 G §439, §409). + Bodies map[string]map[string]any `json:"bodies,omitempty"` // Acts are the seat's acting rules (novox/hq ADR 0656 §1; design 47 *What ADR 0656 leaves to this design*): each step // of a delivery the seat's holder acts in, on which modules, and how long an entry waits there for the holder's new // version. At either grain: a seat-list definition names its holder's step too. @@ -215,6 +218,9 @@ type Verb struct { Callers *Callers `json:"callers,omitempty"` // Class is the verb's class, ClassRead, ClassAct or ClassWarrant (design 47 G §391). Class string `json:"class,omitempty"` + // NeedsApproval marks the verb for every holder: the operator approves a call of it first (design 46 *The approval + // mark* §2). + NeedsApproval bool `json:"needs-approval,omitempty"` } // Message is one accepted message or one event of the seat, its body a JSON Schema. diff --git a/go/seatdef/seatdef_test.go b/go/seatdef/seatdef_test.go index c284153..6fb8b8b 100644 --- a/go/seatdef/seatdef_test.go +++ b/go/seatdef/seatdef_test.go @@ -140,6 +140,9 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) { {"the step pipeline and an image rule", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"pipeline"},{"step":"build","on":"kind:image"},{"step":"check"}],"verbs":`, 1), ""}, {"an acting rule on another kind", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"build","on":"kind:Image"}],"verbs":`, 1), "acts[0].on"}, + {"a verb marked for approval", strings.Replace(whole, `"name":"list",`, `"name":"list","needs-approval":true,`, 1), ""}, + {"a verb marked with a word that is no boolean", strings.Replace(whole, `"name":"list",`, `"name":"list","needs-approval":"yes",`, 1), "not a seat definition's JSON"}, + {"a bodies section", strings.Replace(whole, `"family":`, `"bodies":{"Finding":{"type":"object"}},"family":`, 1), ""}, {"over 256 KiB", listGrain[:len(listGrain)-1] + `,"description":"` + strings.Repeat("x", MaxSize) + `"}`, ErrTooLarge.Error()}, }