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 `` 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, "") { 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] }