1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
|
// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
// SPDX-License-Identifier: Apache-2.0
package path
import (
"encoding/json"
"fmt"
"strconv"
"strings"
. "go.xyrillian.de/gg/option"
)
// Path is used to identify the current location within a nested data structure
// while recursing through it. For example, when comparing
//
// actual = { "foo": { "bar": [ 5, 23 ] } }
// expected = { "foo": { "bar": [ 5, 42 ] } }
//
// we would generate a diff at the path {"foo", "bar", 1}.
// Since diffs are usually rare, we only build Pointer strings
// out of these paths when we really need them.
// During recursion, Path holds a sequence of path elements,
// most of which are constants to keep allocations to a minimum.
//
// # Warning
//
// Because the same Path slice is heavily reused across nested function calls,
// it is not safe to store references to the Path slice during such a recursion.
type Path []Element
// Path preallocates a decently sized path buffer for use in a recursion.
func NewPath() Path {
return make([]Element, 0, 32)
}
// Element occurs in type [Path]. Only one of both fields is set per instance.
type Element struct {
// used by both package jsonmatch and package assert
Key Option[string]
Index int
// only used by package assert (panics when used with AsJSONPointer())
MapKey Option[any] // holds a value of type K for traversing into types ~map[K]V
TypeCast string // holds a %T formatting of a type
Dereference bool // marks when a pointer is dereferenced
}
// KeyElement is a shorthand for constructing an Element with the Key field set.
func KeyElement(key string) Element { return Element{Key: Some(key)} }
// IndexElement is a shorthand for constructing an Element with the Index field set.
func IndexElement(idx int) Element { return Element{Index: idx} }
// MapKeyElement is a shorthand for constructing an Element with the MapKey field set.
func MapKeyElement(key any) Element { return Element{MapKey: Some(key)} }
// TypeCastElement is a shorthand for constructing an Element with the TypeCast field set.
func TypeCastElement(typeStr string) Element { return Element{TypeCast: typeStr} }
// DereferenceElement is a shorthand for constructing an Element with the Dereference field set.
func DereferenceElement() Element { return Element{Dereference: true} }
// AsJSONPointer serializes p as a JSON pointer (RFC 6901).
func (p Path) AsJSONPointer() string {
if len(p) == 0 {
return ""
}
fragments := make([]string, len(p)+1)
fragments[0] = ""
for idx, elem := range p {
if elem.Dereference {
panic("Dereference elements cannot be used with AsJSONPointer()")
}
if elem.TypeCast != "" {
panic("TypeCast elements cannot be used with AsJSONPointer()")
}
if elem.MapKey.IsSome() {
panic("MapKey elements cannot be used with AsJSONPointer()")
}
if key, ok := elem.Key.Unpack(); ok {
fragments[idx+1] = keyIntoPointerFragment(key)
} else {
fragments[idx+1] = strconv.Itoa(elem.Index)
}
}
return strings.Join(fragments, "/")
}
func keyIntoPointerFragment(key string) string {
buf, _ := json.Marshal(key)
s := string(buf)
s = strings.TrimPrefix(s, "\"")
s = strings.TrimSuffix(s, "\"")
s = strings.ReplaceAll(s, "~", "~0")
s = strings.ReplaceAll(s, "/", "~1")
return s
}
// AsGoExpression serializes p as a partial Go expression like `value.Objects["foo.txt"].Lines[42]`.
func (p Path) AsGoExpression(baseVariable string) string {
b := &strings.Builder{}
fmt.Fprint(b, baseVariable)
for idx, elem := range p {
if elem.Dereference {
if idx != len(p)-1 && p[idx+1].Key.IsSome() {
// simplify `(*foo).Bar` to `foo.Bar`
continue
}
str := b.String()
b = &strings.Builder{}
fmt.Fprintf(b, "(*%s)", str)
} else if elem.TypeCast != "" {
fmt.Fprintf(b, `.(%s)`, elem.TypeCast)
} else if mapKey, ok := elem.MapKey.Unpack(); ok {
fmt.Fprintf(b, `[%#v]`, mapKey)
} else if key, ok := elem.Key.Unpack(); ok {
fmt.Fprintf(b, `.%s`, key)
} else {
fmt.Fprintf(b, `[%d]`, elem.Index)
}
}
return b.String()
}
|