diff options
Diffstat (limited to 'internal/path/path.go')
| -rw-r--r-- | internal/path/path.go | 74 |
1 files changed, 74 insertions, 0 deletions
diff --git a/internal/path/path.go b/internal/path/path.go new file mode 100644 index 0000000..30a94d2 --- /dev/null +++ b/internal/path/path.go @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: Apache-2.0 + +package path + +import ( + "encoding/json" + "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 { + Key Option[string] + Index int +} + +// KeyElement is a shorthand for constructing an Element with the Key field set. +func KeyElement(key string) Element { return Element{Some(key), 0} } + +// IndexElement is a shorthand for constructing an Element with the Index field set. +func IndexElement(idx int) Element { return Element{None[string](), idx} } + +// 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 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 +} |
