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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
|
// 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())
Slice Option[SliceBounds]
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
}
// SliceBounds appears in type [Element].
type SliceBounds struct {
Start int
End int
}
// 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} }
// SliceElement is a shorthand for constructing an Element with the Slice field set.
func SliceElement(start, end int) Element { return Element{Slice: Some(SliceBounds{start, end})} }
// 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 elem.Slice.IsSome() {
panic("Slice 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 bounds, ok := elem.Slice.Unpack(); ok {
fmt.Fprintf(b, `[%d:%d]`, bounds.Start, bounds.End)
} else if key, ok := elem.Key.Unpack(); ok {
fmt.Fprintf(b, `.%s`, key)
} else {
fmt.Fprintf(b, `[%d]`, elem.Index)
}
}
return b.String()
}
|