diff --git a/go/catalogue/manifest_reads.go b/go/catalogue/manifest_reads.go index ac69427..98503a1 100644 --- a/go/catalogue/manifest_reads.go +++ b/go/catalogue/manifest_reads.go @@ -163,12 +163,8 @@ func ManifestNeeds(raw []byte) ([]string, error) { return nil, err } used := map[string]bool{} - for _, f := range referenceForms { - if bytes.Contains(raw, []byte("${"+f+":")) { - used[ReadForm+f] = true - } - } - if ofSeatReach.Match(raw) || claimsAtMesh(top, ContainerImageRegistrySeat) { + textNeeds(raw, used) + if claimsAtMesh(top, ContainerImageRegistrySeat) { used[ReadForm+ReferenceFormSeatReach] = true } if _, has := top[ConfigField]; has || takesConfigKey.Match(raw) { @@ -429,6 +425,39 @@ func asList(v any) []any { return list } +// textNeeds adds the reference forms a manifest's or a template's text writes to used: a form the engine fills in a +// file's text is read there whether the text is inline or in a template file. +func textNeeds(text []byte, used map[string]bool) { + for _, f := range referenceForms { + if bytes.Contains(text, []byte("${"+f+":")) { + used[ReadForm+f] = true + } + } + if ofSeatReach.Match(text) { + used[ReadForm+ReferenceFormSeatReach] = true + } +} + +// ModuleNeeds is ManifestNeeds of a module whose files take their text from templates (template.go): what the manifest +// needs and what each template's text writes, by template path (TemplatePaths), sorted. A module's needs are both: an +// engine renders the template's text as it renders inline content, so a template naming ${seat:x:reach} needs the +// reading as the same content inline does, though the manifest itself is unchanged (novox/hq ADR 0632 §2). A template +// named and not given is no need; the reader of the files says it could not read one. +func ModuleNeeds(raw []byte, templates map[string]string) ([]string, error) { + needs, err := ManifestNeeds(raw) + if err != nil || len(templates) == 0 { + return needs, err + } + used := map[string]bool{} + for _, n := range needs { + used[n] = true + } + for _, text := range templates { + textNeeds([]byte(text), used) + } + return sortedSet(used), nil +} + func sortedSet(set map[string]bool) []string { out := make([]string, 0, len(set)) for k := range set { diff --git a/go/catalogue/own_manifest_test.go b/go/catalogue/own_manifest_test.go index ce279b0..21ae5f8 100644 --- a/go/catalogue/own_manifest_test.go +++ b/go/catalogue/own_manifest_test.go @@ -1,6 +1,7 @@ package catalogue import ( + "maps" "os" "slices" "testing" @@ -87,12 +88,26 @@ func TestOwnManifestUsesNoWordTheRunningControllerRefuses(t *testing.T) { {"the store's holder", `{"module":"distribution","version":"1","claims":[{"name":"container-image-registry",` + `"scope":"mesh","serves":["list"]}],"listens":[{"name":"registry","port":5000,"from":"mesh"}]}`, []string{"form:seat-reach", "seat:container-image-registry"}, []string{"form:seat-reach", "seat:container-image-registry"}}, + // docker's daemon file as ADR 0632 leaves it: the manifest unchanged, its template naming the image seat's reach; + // TemplatePaths names the template a reader of the files reads. + {"a template naming a seat's reach", `{"module":"d","version":"1","resources":[{"id":"daemon","type":"file",` + + `"path":"/etc/d.json","template":"files/daemon.json"}]}`, []string{"form:seat", "form:seat-reach"}, + []string{"form:seat-reach"}}, {"a word this contract does not know", `{"module":"u","version":"1","newer-word":{"inside":1}}`, []string{"word:module"}, nil}, } { - needs, err := ManifestNeeds([]byte(row.raw)) + templates := map[string]map[string]string{ // the templates' text a row's module names + "a template naming a seat's reach": {"files/daemon.json": `{"x":"${seat:container-image-registry:reach}"}`}, + }[row.why] + needs, err := ModuleNeeds([]byte(row.raw), templates) if err != nil { t.Fatalf("%s: %v", row.why, err) } + if templates != nil { + m, err := Decode([]byte(row.raw)) + if paths := TemplatePaths(m); err != nil || !slices.Equal(paths, slices.Sorted(maps.Keys(templates))) { + t.Errorf("%s: TemplatePaths = %v (%v), want %v", row.why, paths, err, slices.Sorted(maps.Keys(templates))) + } + } for _, n := range row.needs { if !slices.Contains(needs, n) { t.Errorf("%s: needs %v, not %s", row.why, needs, n) diff --git a/go/catalogue/template.go b/go/catalogue/template.go index d5d8945..53d849f 100644 --- a/go/catalogue/template.go +++ b/go/catalogue/template.go @@ -8,6 +8,7 @@ import ( "os" "path" "path/filepath" + "slices" "strings" "unicode/utf8" ) @@ -184,6 +185,25 @@ func fillItemTemplates(m Manifest, items []Item, text func(string) (string, bool return out, nil } +// TemplatePaths is every template the manifest's files and items name, by its path in the module's directory, sorted +// and once each: what a reader of the module's files reads for ModuleNeeds. A template whose name is wrong is left out; +// the judge refuses it by name. +func TemplatePaths(m Manifest) []string { + var out []string + for _, r := range m.Resources { + if name, has, wrong := templateOf(m.Module, m.TemplatesDir(), r); has && wrong == "" { + out = append(out, name) + } + } + for _, it := range m.Items { + if it.Template != "" && itemTemplateProblem(m, it.Template) == "" { + out = append(out, it.Template) + } + } + slices.Sort(out) + return slices.Compact(out) +} + // ReadTemplates is the text of each template the manifest names, read from the module's directory in a repository tree, // by template path. Refused by name: a template the directory lacks, and anything under its templates directory // (TemplatesDir) the build's packing refuses — that directory itself, or any directory or file under it, that is a diff --git a/go/delivery/source.go b/go/delivery/source.go index 6eebdfb..500dfd8 100644 --- a/go/delivery/source.go +++ b/go/delivery/source.go @@ -594,6 +594,10 @@ type Forge struct { // relative to it (novox/hq ADR 0465 clause 1). Older writers send neither. Manifests map[string]string `json:"manifests,omitempty"` Locks map[string]map[string]string `json:"locks,omitempty"` + // Needs are, of each such directory whose manifest names templates, the reader capabilities of the module whole: + // its manifest's and its templates' text's at the same commit (catalogue.ModuleNeeds; novox/hq ADR 0632 §2). The + // indexer takes them over the manifest's own; a directory absent, or an older writer, has the manifest's alone. + Needs map[string][]string `json:"needs,omitempty"` // Paths are, of each module directory whose manifest names paths on every artifact, each named path's git object // id at the same commit as its manifest, by path relative to the directory (novox/hq ADR 0499 clause 3). The // indexer names the paths; a message without them gives such a module none. Older writers send none.