Take the catalogue's composition rules into the node-engine
ADR 0593 §7: the composer with its refusals leaves mesh-sdk, so mesh-sdk carries no composition rule. The catalogue functions only the node-engine's composition reaches (Resolve, Resolution's Compose, Declaration, DataItems, LeftOut, Rules and the rest, the settings, placeholder and template refusals, the rendering helpers) are copied with their tests into internal/compose/resolution; a method of a mesh-sdk shape becomes a function taking it first. The composition calls its own copies, and the captured catalogue its tests read lives here now, shared by mesh-compose's tests. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
package resolution
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// collectableAsText is whether a collector may name a kind received as data as a collection.
|
||||
func collectableAsText(r mcat.Receivable) bool { return r.Shape != nil && r.Shape.Text }
|
||||
|
||||
// renderFuncs are the functions a holder's template may call. `quote` writes a value as a JSON
|
||||
// string with DEL escaped as well — JSON leaves it bare, and TOML refuses it — which makes it a valid
|
||||
// basic string in TOML and in most tools' grammars, so a contributed command with quotes or control
|
||||
// characters in it cannot break out of the holder's line.
|
||||
var renderFuncs = template.FuncMap{"quote": quote}
|
||||
|
||||
func quote(v any) (string, error) {
|
||||
b, err := json.Marshal(fmt.Sprint(v))
|
||||
return strings.ReplaceAll(string(b), "\x7f", `\u007f`), err
|
||||
}
|
||||
|
||||
// wholeIn is v as a whole number within [lo, hi]; JSON gives every number as a float.
|
||||
func wholeIn(v any, lo, hi int) (int, bool) {
|
||||
f, ok := v.(float64)
|
||||
if !ok {
|
||||
if i, isInt := v.(int); isInt {
|
||||
f, ok = float64(i), true
|
||||
}
|
||||
}
|
||||
if !ok || f != math.Trunc(f) || f < float64(lo) || f > float64(hi) {
|
||||
return 0, false
|
||||
}
|
||||
return int(f), true
|
||||
}
|
||||
|
||||
// placeholderWhere parses the narrowing part of a placeholder, `bar=bottom,place=status`, against the
|
||||
// kind's shape. Empty narrows nothing.
|
||||
func placeholderWhere(r mcat.Receivable, where string) (map[string]string, error) {
|
||||
out := map[string]string{}
|
||||
if where == "" {
|
||||
return out, nil
|
||||
}
|
||||
if r.Shape == nil {
|
||||
return nil, fmt.Errorf("%s is text, which a placeholder does not narrow", r.Kind)
|
||||
}
|
||||
for _, pair := range strings.Split(where, ",") {
|
||||
field, value, ok := strings.Cut(pair, "=")
|
||||
var p *mcat.PlaceField
|
||||
for i := range r.Shape.Places {
|
||||
if r.Shape.Places[i].Field == field {
|
||||
p = &r.Shape.Places[i]
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case !ok || p == nil:
|
||||
return nil, fmt.Errorf("%q narrows by %s, and %s is placed by %s", where, field, r.Kind, placeFields(r))
|
||||
case !oneOf(p.Values, value):
|
||||
return nil, fmt.Errorf("%q says %s=%s; %s is one of %s", where, field, value, field, strings.Join(p.Values, ", "))
|
||||
case out[field] != "":
|
||||
return nil, fmt.Errorf("%q names %s twice", where, field)
|
||||
}
|
||||
out[field] = value
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func placeFields(r mcat.Receivable) string {
|
||||
var names []string
|
||||
for _, p := range r.Shape.Places {
|
||||
names = append(names, p.Field)
|
||||
}
|
||||
return strings.Join(names, ", ")
|
||||
}
|
||||
|
||||
// holderTemplate is the holder's template for a kind, parsed.
|
||||
func holderTemplate(holder mcat.Manifest, seat mcat.Seat, kind string) (*template.Template, error) {
|
||||
for _, c := range holder.Claims {
|
||||
if s, known := mcat.SeatNamed(c.Name); !known || s.Name != seat.Name {
|
||||
continue
|
||||
}
|
||||
text, has := c.Renders[kind]
|
||||
if !has || strings.TrimSpace(text) == "" {
|
||||
break
|
||||
}
|
||||
t, err := template.New(seat.Name + ":" + kind).Funcs(renderFuncs).Parse(text)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s's template for %s:%s does not parse: %v", holder.Module, seat.Name, kind, err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%s places %s:%s, which is data, and its claim gives no template for it in `renders` "+
|
||||
"(novox/hq ADR 0255)", holder.Module, seat.Name, kind)
|
||||
}
|
||||
|
||||
// renderPiece is one piece of data in the holder's grammar. The template sees the piece's fields,
|
||||
// `module`, the contributor, and `machine`, the node it is composed for. A value the piece does not have reads as nothing in a `with` or an `if`;
|
||||
// printed bare it would write `<no value>` into the tool's file, which is refused here instead.
|
||||
func renderPiece(t *template.Template, module, machine string, data map[string]any) (string, error) {
|
||||
view := map[string]any{"module": module, "machine": machine}
|
||||
for k, v := range data {
|
||||
view[k] = v
|
||||
}
|
||||
var b bytes.Buffer
|
||||
if err := t.Execute(&b, view); err != nil {
|
||||
return "", fmt.Errorf("rendering %s's piece: %v", module, err)
|
||||
}
|
||||
out := b.String()
|
||||
if strings.Contains(out, "<no value>") {
|
||||
return "", fmt.Errorf("rendering %s's piece printed a value it does not have: %q", module, out)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// placedPiece is one contributed piece on its way into a holder's file.
|
||||
type placedPiece struct {
|
||||
module string
|
||||
data map[string]any
|
||||
order int
|
||||
// text is a moved contributor's item, rendered (collections.go heldItems): placed as it is, not through the
|
||||
// holder's template.
|
||||
text string
|
||||
item bool
|
||||
}
|
||||
|
||||
// shapedContributions is every module's data of one kind to one seat, narrowed by where and by the
|
||||
// machine's capabilities, ordered, and rendered through the holder's template — each piece under a
|
||||
// comment line naming its module.
|
||||
//
|
||||
// A piece the template renders to nothing — a `shows` the holder does not know yet — or fails on is
|
||||
// left out and answered as unplaced, naming its module: the other pieces and the rest of the machine's
|
||||
// declaration go on (novox/hq ADR 0255).
|
||||
func shapedContributions(modules []mcat.Manifest, holder mcat.Manifest, machine string, facts map[string]string, s mcat.Seat, r mcat.Receivable, where map[string]string, caps map[string]bool) (string, []string, error) {
|
||||
var pieces []placedPiece
|
||||
for _, m := range inModuleOrder(modules) {
|
||||
for _, c := range manifestAllContributions(m) {
|
||||
if c.Kind != r.Kind || !capable(c, caps) {
|
||||
continue
|
||||
}
|
||||
if cs, known := mcat.SeatNamed(c.Seat); !known || cs.Name != s.Name {
|
||||
continue
|
||||
}
|
||||
match := true
|
||||
for field, value := range where {
|
||||
if v, _ := c.Data[field].(string); v != value {
|
||||
match = false
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
order := mcat.OrderDefault
|
||||
if v, ok := wholeIn(c.Data[r.Shape.Order], 0, 99); ok {
|
||||
order = v
|
||||
}
|
||||
pieces = append(pieces, placedPiece{module: m.Module, data: c.Data, order: order})
|
||||
}
|
||||
// A moved contributor's items of the kind, by their attributes (design 57 L3, issue 777): each in its
|
||||
// module's place, by its order, then its name.
|
||||
if !r.Dirs {
|
||||
for _, h := range heldItems(m, "contribution:"+s.Name+":"+r.Kind, where, facts, caps) {
|
||||
pieces = append(pieces, placedPiece{module: m.Module, order: itemOrder(h.item), text: h.text, item: true})
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(pieces) == 0 {
|
||||
return "", nil, nil
|
||||
}
|
||||
var t *template.Template
|
||||
for _, p := range pieces {
|
||||
if p.item {
|
||||
continue
|
||||
}
|
||||
var err error
|
||||
if t, err = holderTemplate(holder, s, r.Kind); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
break
|
||||
}
|
||||
sort.SliceStable(pieces, func(a, b int) bool { return pieces[a].order < pieces[b].order })
|
||||
var b strings.Builder
|
||||
var unplaced []string
|
||||
for _, p := range pieces {
|
||||
if p.item {
|
||||
fmt.Fprintf(&b, "%s %s\n", r.Comment, p.module)
|
||||
b.WriteString(p.text)
|
||||
continue
|
||||
}
|
||||
out, err := renderPiece(t, p.module, machine, p.data)
|
||||
if err == nil && strings.TrimSpace(out) == "" {
|
||||
err = fmt.Errorf("%s's template renders nothing for it", holder.Module)
|
||||
}
|
||||
if err != nil {
|
||||
unplaced = append(unplaced, fmt.Sprintf("%s's %s:%s %s is not placed by %s: %v",
|
||||
p.module, s.Name, r.Kind, describePiece(p.data), holder.Module, err))
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&b, "%s %s\n", r.Comment, p.module)
|
||||
b.WriteString(out)
|
||||
if !strings.HasSuffix(out, "\n") {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return b.String(), unplaced, nil
|
||||
}
|
||||
|
||||
// describePiece is a piece in one line, its fields in order, for a person reading why it was left out.
|
||||
func describePiece(data map[string]any) string {
|
||||
var parts []string
|
||||
for _, k := range sortedKeys(data) {
|
||||
if _, nested := data[k].(map[string]any); nested {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%s=%v", k, data[k]))
|
||||
}
|
||||
return "(" + strings.Join(parts, " ") + ")"
|
||||
}
|
||||
|
||||
// capable is whether a contribution applies on a machine with these capabilities: always, unless it
|
||||
// names one with `if-capability` the machine did not report (novox/hq ADR 0255). A machine whose
|
||||
// capabilities are not known has none, so a conditional piece is left out rather than guessed in.
|
||||
func capable(c mcat.SeatContribution, caps map[string]bool) bool {
|
||||
return c.IfCapability == "" || caps[c.IfCapability]
|
||||
}
|
||||
Reference in New Issue
Block a user