aboutsummaryrefslogtreecommitdiff
path: root/internal/path/path.go
blob: 30a94d2e350985ba34484521aedc4448ee2272e8 (plain)
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
// 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
}