package resolution import ( "fmt" "regexp" "sort" "strings" mcat "git.novox.be/novox/mesh-sdk/go/catalogue" ) // bound is where a module says a value from one of its bindings belongs: // ${bound::}, and for an optional requirement the value it takes while absent, // ${bound::|default:} (novox/hq ADR 0330 §4). A default is never empty: an // absent requirement is never rendered as an empty string. var bound = regexp.MustCompile(`\$\{bound:([a-z0-9][a-z0-9.-]*[a-z0-9]):([a-z0-9][a-z0-9_-]*)(?:\|default:([^}\n]+))?\}`) // knownFor is everything a module may name from one of its bindings. // // Three facts the mesh states about any provision, plus whatever the provider said it serves. A // module may not reach a binding it does not have — the same boundary as a secret, for the same // reason. func knownFor(m mcat.Manifest, needs []mcat.Needed, node string) (map[string]map[string]string, error) { out := map[string]map[string]string{} for _, want := range m.Wants() { for i := range needs { n := needs[i] if n.Name != want || n.For != m.Module { continue } as := mcat.ConsumerIdentity(node, mcat.IdentitySource(m.Slug, m.Module)) values := map[string]string{ "at": n.At, "from": n.From, "as": as, } // What the provider derives for this consumer rather than for all of them // (novox/hq ADR 0202). Filled here, the one place a provision and the module // requiring it are both in hand. served, err := ServedTo(n.Serves, as) if err != nil { return nil, fmt.Errorf("%s requires %s: %w", m.Module, want, err) } for key, value := range served { // The provider's own vocabulary. Rendered plainly: a port is 5432, not 5432.000000, // which is what a float would write and what a connection string would refuse. values[key] = plainly(value) } out[want] = values } } return out, nil } // withOwnNames adds a module's own composed names to what it may name from one binding: // `${bound::name}` and `:internal-name`, and for several contributions to one requirement // `:name-` / `:internal-name-`. Set over anything the provider serves under those keys: // what the module is called is the mesh's statement, not the provider's. func withOwnNames(values map[string]string, own map[string]any) { for _, key := range []string{"name", "internal-name"} { if v, ok := own[key].(string); ok { values[key] = v } } many, _ := own["names"].(map[string]any) for local, raw := range many { names, _ := raw.(map[string]any) for _, key := range []string{"name", "internal-name"} { if v, ok := names[key].(string); ok { values[key+"-"+local] = v } } } } // plainly renders a served value as a program would expect to read it. func plainly(value any) string { switch v := value.(type) { case string: return v case float64: if v == float64(int64(v)) { return fmt.Sprintf("%d", int64(v)) } return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%f", v), "0"), ".") case bool: return fmt.Sprintf("%t", v) case nil: return "" default: return fmt.Sprint(v) } } // boundInto replaces a file's ${bound:…} placeholders with what the mesh knows. // // A placeholder naming something the module does not require, or a key the provider does not // serve, is refused. Left as it was, the literal `${bound:x:y}` would be written into a // configuration file and read as a value — a connection to a host called `${bound:x:y}`, failing // somewhere that names neither the module nor the mesh. // // **An optional requirement left absent takes the default its placeholder names** (novox/hq ADR // 0330 §4), and one that names none is refused rather than rendered empty. The module check refuses // that already; this is the same rule where the file is made, for a manifest registered before it. func boundInto(resource map[string]any, known map[string]map[string]string, module string, absent map[string]bool) error { if fmt.Sprint(resource["type"]) != "file" { return nil } content, ok := resource["content"].(string) if !ok { return nil } var refused error filled := bound.ReplaceAllStringFunc(content, func(token string) string { if refused != nil { return token } m := bound.FindStringSubmatch(token) provision, key, fallback := m[1], m[2], m[3] values, has := known[provision] if !has { if absent[provision] { if fallback != "" { return fallback } refused = fmt.Errorf( "%s has a file that says ${bound:%s:%s}, and %q is optional and absent here: it names no "+ "value for then (${bound:%s:%s|default:}), so it would be written empty "+ "(novox/hq ADR 0330 §4)", module, provision, key, provision, provision, key) return token } refused = fmt.Errorf( "%s has a file that says ${bound:%s:%s}, and %s does not require %q. It may name %s", module, provision, key, module, provision, orNothing(namesOfBindings(known))) return token } value, said := values[key] if !said { refused = fmt.Errorf( "%s asks its %s binding for %q, and what answers it says %s", module, provision, key, orNothing(namesOfKeys(values))) return token } return value }) if refused != nil { return refused } resource["content"] = filled return nil } func namesOfBindings(known map[string]map[string]string) []string { var names []string for name := range known { names = append(names, fmt.Sprintf("%q", name)) } sort.Strings(names) return names } func namesOfKeys(values map[string]string) []string { var names []string for key := range values { names = append(names, fmt.Sprintf("%q", key)) } sort.Strings(names) return names } func orNothing(names []string) string { if len(names) == 0 { return "nothing" } return join(names) }