Files
mesh-sdk/go/delivery/answer_paths.go
T

137 lines
3.5 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):
// A file's secrets (SecretsField) are named entry by entry, each entry never entered; any other
// object under a secret key, a vault box included, is one path.
sx, isSX := x.(map[string]any)
sy, isSY := y.(map[string]any)
if k != SecretsField || !isSX || !isSY {
if !sameAny(x, y) {
*out = append(*out, join(at, k))
}
continue
}
for name := range sx {
if v, in := sy[name]; !in || !sameAny(sx[name], v) {
*out = append(*out, join(join(at, k), name))
}
}
for name := range sy {
if _, in := sx[name]; !in {
*out = append(*out, join(join(at, k), name))
}
}
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)
}