-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathpointer.go
185 lines (163 loc) · 5.12 KB
/
pointer.go
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// Package jsonpointer implements IETF rfc6901
// JSON Pointers are a string syntax for
// identifying a specific value within a JavaScript Object Notation
// (JSON) document [RFC4627]. JSON Pointer is intended to be easily
// expressed in JSON string values as well as Uniform Resource
// Identifier (URI) [RFC3986] fragment identifiers.
//
// this package is intended to work like net/url from the go
// standard library
package jsonpointer
import (
"fmt"
"net/url"
"strconv"
"strings"
)
const defaultPointerAllocationSize = 32
// Parse parses str into a Pointer structure.
// str may be a pointer or a url string.
// If a url string, Parse will use the URL's fragment component
// (the bit after the '#' symbol)
func Parse(str string) (Pointer, error) {
// fast paths that skip url parse step
if len(str) == 0 || str == "#" {
return Pointer{}, nil
} else if str[0] == '/' {
return parse(str)
}
u, err := url.Parse(str)
if err != nil {
return nil, err
}
return parse(u.Fragment)
}
// IsEmpty is a utility function to check if the Pointer
// is empty / nil equivalent
func (p Pointer) IsEmpty() bool {
return len(p) == 0
}
// Head returns the root of the Pointer
func (p Pointer) Head() *string {
if len(p) == 0 {
return nil
}
return &p[0]
}
// Tail returns everything after the Pointer head
func (p Pointer) Tail() Pointer {
return Pointer(p[1:])
}
// The ABNF syntax of a JSON Pointer is:
// json-pointer = *( "/" reference-token )
// reference-token = *( unescaped / escaped )
// unescaped = %x00-2E / %x30-7D / %x7F-10FFFF
// ; %x2F ('/') and %x7E ('~') are excluded from 'unescaped'
// escaped = "~" ( "0" / "1" )
// ; representing '~' and '/', respectively
func parse(str string) (Pointer, error) {
if len(str) == 0 {
return Pointer{}, nil
}
if str[0] != '/' {
return nil, fmt.Errorf("non-empty references must begin with a '/' character")
}
str = str[1:]
toks := strings.Split(str, separator)
for i, t := range toks {
toks[i] = unescapeToken(t)
}
return Pointer(toks), nil
}
// Pointer represents a parsed JSON pointer
type Pointer []string
// NewPointer creates a Pointer with a pre-allocated block of memory
// to avoid repeated slice expansions
func NewPointer() Pointer {
return make([]string, 0, defaultPointerAllocationSize)
}
// String implements the stringer interface for Pointer,
// giving the escaped string
func (p Pointer) String() (str string) {
for _, tok := range p {
str += "/" + escapeToken(tok)
}
return
}
// Eval evaluates a json pointer against a given root JSON document
// Evaluation of a JSON Pointer begins with a reference to the root
// value of a JSON document and completes with a reference to some value
// within the document. Each reference token in the JSON Pointer is
// evaluated sequentially.
func (p Pointer) Eval(data interface{}) (result interface{}, err error) {
result = data
for _, tok := range p {
if result, err = p.evalToken(tok, result); err != nil {
return nil, err
}
}
return
}
// Descendant returns a new pointer to a descendant of the current pointer
// parsing the input path into components
func (p Pointer) Descendant(path string) (Pointer, error) {
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
dpath, err := parse(path)
if err != nil {
return p, err
}
if p.String() == "/" {
return dpath, nil
}
return append(p, dpath...), nil
}
// RawDescendant extends the pointer with 1 or more path tokens
// The function itself is unsafe as it doesnt fully parse the input
// and assumes the user is directly managing the pointer
// This allows for much faster pointer management
func (p Pointer) RawDescendant(path ...string) Pointer {
return append(p, path...)
}
// Evaluation of each reference token begins by decoding any escaped
// character sequence. This is performed by first transforming any
// occurrence of the sequence '~1' to '/', and then transforming any
// occurrence of the sequence '~0' to '~'. By performing the
// substitutions in this order, an implementation avoids the error of
// turning '~01' first into '~1' and then into '/', which would be
// incorrect (the string '~01' correctly becomes '~1' after
// transformation).
// The reference token then modifies which value is referenced according
// to the following scheme:
func (p Pointer) evalToken(tok string, data interface{}) (interface{}, error) {
switch ch := data.(type) {
case map[string]interface{}:
return ch[tok], nil
case []interface{}:
i, err := strconv.Atoi(tok)
if err != nil {
return nil, fmt.Errorf("invalid array index: %s", tok)
}
if i >= len(ch) {
return nil, fmt.Errorf("index %d exceeds array length of %d", i, len(ch))
}
return ch[i], nil
default:
return nil, fmt.Errorf("invalid JSON pointer: %s", p.String())
}
}
const (
separator = "/"
escapedSeparator = "~1"
tilde = "~"
escapedTilde = "~0"
)
func unescapeToken(tok string) string {
tok = strings.Replace(tok, escapedSeparator, separator, -1)
return strings.Replace(tok, escapedTilde, tilde, -1)
}
func escapeToken(tok string) string {
tok = strings.Replace(tok, tilde, escapedTilde, -1)
return strings.Replace(tok, separator, escapedSeparator, -1)
}