Files
mesh-sdk/go/delivery/answer_paths.go
T
jschoubben 16b8d8a1ee Compose as the controller does, so a node's shadow can answer same (issue 1126, C1 C2 C3 C5 C6, T1)
Seals are reused and compared by slot (resource, field), never across resources;
one rule says which resolved config values are laid; a seat path that steps back
is owned by the seat it steps back for, whatever the seat order; a folded module
of an old-form build is composed from what runs of it; the operator export's file
is carried as it runs; a differing resource names its differing JSON paths, found
before its bodies are cut, and an answer is fitted to the node's key.
2026-10-28 09:36:36 +01:00

161 lines
4.2 KiB
Go

package delivery
import (
"bytes"
"encoding/json"
"sort"
"strconv"
"strings"
)
// DifferingPaths is every JSON path at which two bodies of one resource differ, sorted (issue 1126, T1: the shadow
// shows its work): object keys and list places joined by dots (`content.url`, `links.0`), a string that holds a JSON
// object or list read as that JSON and walked on (a runtime's config carried as text), a key or place one side alone
// holds named as it is. A key a secret could be under (secretWords) is named and not walked into. A body that is not
// JSON differs at "" (the whole), and a resource one side alone holds has none. At most MaxPaths: the last then says how many more there are. A path names keys
// alone, never a value, so it holds no secret.
func DifferingPaths(composed, running json.RawMessage) []string {
if composed == nil || running == nil {
return nil // one side alone holds it: its why says so
}
var a, b any
okA := unmarshalNumbers(composed, &a) == nil
okB := unmarshalNumbers(running, &b) == nil
if !okA || !okB {
if bytes.Equal(composed, running) {
return nil
}
return []string{""}
}
var out []string
walkDiffering(a, b, "", &out)
sort.Strings(out)
if len(out) > MaxPaths {
more := len(out) - (MaxPaths - 1)
out = append(out[:MaxPaths-1], "… "+strconv.Itoa(more)+" more")
}
return out
}
func unmarshalNumbers(raw []byte, into *any) error {
d := json.NewDecoder(bytes.NewReader(raw))
d.UseNumber()
return d.Decode(into)
}
// asJSON is a string holding a JSON object or list read as that value; v itself otherwise.
func asJSON(v any) any {
s, ok := v.(string)
if !ok {
return v
}
t := strings.TrimSpace(s)
if t == "" || (t[0] != '{' && t[0] != '[') {
return v
}
var parsed any
if unmarshalNumbers([]byte(t), &parsed) != nil {
return v
}
return parsed
}
func join(at, key string) string {
if at == "" {
return key
}
return at + "." + key
}
func walkDiffering(a, b any, at string, out *[]string) {
a, b = asJSON(a), asJSON(b)
ma, isMA := a.(map[string]any)
mb, isMB := b.(map[string]any)
if isMA && isMB {
keys := map[string]bool{}
for k := range ma {
keys[k] = true
}
for k := range mb {
keys[k] = true
}
for k := range keys {
x, inA := ma[k]
y, inB := mb[k]
switch {
case !inA || !inB:
*out = append(*out, join(at, k))
case secretKey(k):
if !sameAny(x, y) {
*out = append(*out, join(at, k))
}
default:
walkDiffering(x, y, join(at, k), out)
}
}
return
}
la, isLA := a.([]any)
lb, isLB := b.([]any)
if isLA && isLB {
for i := range max(len(la), len(lb)) {
if i >= len(la) || i >= len(lb) {
*out = append(*out, join(at, strconv.Itoa(i)))
continue
}
walkDiffering(la[i], lb[i], join(at, strconv.Itoa(i)), out)
}
return
}
if !sameAny(a, b) {
*out = append(*out, at)
}
}
func sameAny(a, b any) bool {
x, errA := json.Marshal(a)
y, errB := json.Marshal(b)
return errA == nil && errB == nil && bytes.Equal(x, y)
}
// FitAnswer is a with its shown bodies cut until its JSON is within limit bytes (MaxAnswerBytes for the node's key):
// first the bodies' texts, from the last differing module back, keeping their paths; then the paths too. What is cut
// is said in the answer's note. The verdict and every list of modules are never cut.
func FitAnswer(a CandidateAnswer, limit int) CandidateAnswer {
size := func() int {
raw, err := json.Marshal(a)
if err != nil {
return 0
}
return len(raw)
}
if size() <= limit {
return a
}
a.Differs = append([]ModuleDiffers(nil), a.Differs...)
for i := range a.Differs {
a.Differs[i].Bodies = append([]ResourceBodies(nil), a.Differs[i].Bodies...)
}
cutTexts, cutPaths := false, false
for i := len(a.Differs) - 1; i >= 0 && size() > limit; i-- {
for j := range a.Differs[i].Bodies {
a.Differs[i].Bodies[j].Composed, a.Differs[i].Bodies[j].Running = "", ""
}
cutTexts = true
}
for i := len(a.Differs) - 1; i >= 0 && size() > limit; i-- {
for j := range a.Differs[i].Bodies {
a.Differs[i].Bodies[j].Paths = nil
}
cutPaths = true
}
why := "the bodies shown were cut to keep the answer within " + strconv.Itoa(limit>>10) + " KiB"
if cutPaths {
why += ", their paths too"
}
if cutTexts {
a.Note = strings.TrimPrefix(a.Note+"; ", "; ") + why
}
return a
}