summaryrefslogtreecommitdiff
path: root/internal/path/path.go
blob: 333b3b2c64b53fb99504a56cf20222590e314357 (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
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()
}