package catalogue import ( "fmt" "sort" "strings" ) // A provider may tell its consumers its own composed names (novox/hq ADR 0346, issue 624). // // **The name is already the mesh's.** A module contributes a label to a requirement — a route — and // the mesh joins it with the providing machine's domains; the module reads the result in its own files // as `${bound::name}` (novox/hq 04-ISSUES/122). Its consumers could not be told it: a // served value named only the consumer (ADR 0202) and settings. The forge's package registry writes // every tarball at its public name, and a consumer that must trust that name had no source for it but a // second copy kept by hand. So a served value may say `${bound::}` for a requirement // the provider contributes to, and the mesh fills it from the same composition the provider's own files // and its proxy receive. // // **A name the providing machine does not compose leaves the key out** (ADR 0346 §3): a machine with // no public domain has no public name to give, and an empty value or a refusal would be worse than a // consumer told nothing — a refused provider machine takes every provision it offers down with it. The // plan says which key was left out, and why, on the provider's machine and under each consumer's need. // ownNameFacts are what a served value may name of the provider's own composition: the names, as its // own files read them (withOwnNames). var ownNameFacts = []string{"name", "internal-name"} // requirementsContributedTo is every requirement m contributes to, sorted. func requirementsContributedTo(m Manifest) []string { seen := map[string]bool{} for to := range m.Contributes { seen[to] = true } for to := range m.ContributesMany { seen[to] = true } out := make([]string, 0, len(seen)) for to := range seen { out = append(out, to) } sort.Strings(out) return out } // ownNameKeys is every name a served value of m may say for one requirement it contributes to: // `name` and `internal-name`, and `name-` and `internal-name-` for each local name it // contributes under. func ownNameKeys(m Manifest, to string) []string { keys := append([]string{}, ownNameFacts...) for _, local := range sortedKeys(m.ContributesMany[to]) { for _, fact := range ownNameFacts { keys = append(keys, fact+"-"+local) } } return keys } // ServedOwnNames fills the provider's own composed names into what it serves one provision's // consumers, from this resolution's composition of the provider's machine (ADR 0346). // // A value naming a name this machine does not compose is left out, with its key, and said. The map is // copied only when something changes, as ServedTo does: the caller's may be the manifest's. func (r Resolution) ServedOwnNames(m Manifest, provision string, serves map[string]any, layers []Layer) ( map[string]any, []string, error) { var asking []string for _, key := range sortedAnyKeys(serves) { if text, ok := serves[key].(string); ok && bound.MatchString(text) { asking = append(asking, key) } } if len(asking) == 0 { return serves, nil, nil } own := map[string]map[string]string{} for _, to := range requirementsContributedTo(m) { names, err := r.ownNames(m, to, layers) if err != nil { return nil, nil, err } values := map[string]string{} withOwnNames(values, names) own[to] = values } out := make(map[string]any, len(serves)) for k, v := range serves { out[k] = v } var said []string for _, key := range asking { text := serves[key].(string) var missing string filled := bound.ReplaceAllStringFunc(text, func(token string) string { parts := bound.FindStringSubmatch(token) if value := own[parts[1]][parts[2]]; value != "" && parts[3] == "" { return value } if missing == "" { missing = token } return token }) if missing != "" { delete(out, key) said = append(said, fmt.Sprintf( "%s serves %s without %q: it names %s, and %s composes no such name for %s (it has no "+ "public domain, or the endpoint is not reached publicly) (novox/hq ADR 0346)", m.Module, provision, key, missing, orHere(r.Node), m.Module)) continue } out[key] = filled } return out, said, nil } func orHere(node string) string { if node == "" { return "this machine" } return node } // ServedLeftOut is what the providers in this node's own set leave out of what they serve, because // this machine composes no name a served value asks for (ADR 0346 §3): said on the provider's machine, // where the remedy is. func (r Resolution) ServedLeftOut(settings SettingsBy) []string { var said []string for _, m := range r.Modules { for _, provision := range sortedServes(m.Serves) { _, lines, err := r.ServedOwnNames(m, provision, m.Serves[provision], WithDefaults(m, settings[m.Module])) if err != nil { said = append(said, fmt.Sprintf("%s serves %s, and its own names could not be composed: %v", m.Module, provision, err)) continue } said = append(said, lines...) } } return said } // checkServedOwnNames refuses a served `${bound:…}` that names a requirement the module does not // contribute to, a fact other than its own names, or a local name it does not contribute under; and // one with a default, since a served value is left out rather than given one (ADR 0346 §3, §4). func checkServedOwnNames(m Manifest, provision, key, text string) []string { var problems []string contributes := requirementsContributedTo(m) for _, parts := range bound.FindAllStringSubmatch(text, -1) { to, fact, fallback := parts[1], parts[2], parts[3] says := fmt.Sprintf("%s serves %s, and the value it serves as %q says %s", m.Module, provision, key, parts[0]) switch { case fallback != "": problems = append(problems, says+": a served name takes no default; one the machine does not "+ "compose leaves the key out (novox/hq ADR 0346)") case !oneOf(contributes, to): problems = append(problems, fmt.Sprintf( "%s, and %s contributes to %s: a served value names only the provider's own names "+ "(novox/hq ADR 0346)", says, m.Module, orNothing(quoted(contributes)))) case !oneOf(ownNameKeys(m, to), fact): problems = append(problems, fmt.Sprintf("%s, and what %s is called through %s is %s (novox/hq ADR 0346)", says, m.Module, to, strings.Join(ownNameKeys(m, to), ", "))) } } return problems } func quoted(values []string) []string { out := make([]string, len(values)) for i, v := range values { out[i] = fmt.Sprintf("%q", v) } return out } // nestedOwnNames refuses a `${bound:…}` inside a served list or object: only a string value of its own is // filled (ServedOwnNames), so a nested one would reach the consumer as the placeholder (ADR 0346 §5). func nestedOwnNames(m Manifest, provision, key string, value any) []string { var found []string var walk func(v any) walk = func(v any) { switch x := v.(type) { case string: found = append(found, bound.FindAllString(x, -1)...) case []any: for _, e := range x { walk(e) } case map[string]any: for _, k := range sortedAnyKeys(x) { walk(x[k]) } } } walk(value) if len(found) == 0 { return nil } return []string{fmt.Sprintf( "%s serves %s, and the value it serves as %q holds %s inside a list or an object: a provider's own "+ "name is filled only in a string value of its own (novox/hq ADR 0346)", m.Module, provision, key, strings.Join(found, ", "))} }