-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjsonptr.go
More file actions
154 lines (132 loc) · 3.7 KB
/
jsonptr.go
File metadata and controls
154 lines (132 loc) · 3.7 KB
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
// Copyright 2019 Torben Schinke. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package jsonptr
import (
"fmt"
"sort"
"strconv"
"strings"
)
// A Ptr specifies a specific value within a JSON document.
// See https://tools.ietf.org/html/rfc6901 for the specification.
type Ptr = string
// A PtrToken is a single element or token of a Ptr
type PtrToken = string
// Eval takes the json pointer and applies it to the given json object or array.
// Returns an error if the json pointer cannot be resolved.
func Eval(objOrArr ObjOrArr, ptr Ptr) (Value, error) {
if len(ptr) == 0 {
// the whole document selector
return objOrArr, nil
}
if !strings.HasPrefix(ptr, "/") {
return nil, fmt.Errorf("invalid json pointer: %s", ptr)
}
tokens := strings.Split(ptr, "/")[1:] // ignore the first empty token
var root Value
root = objOrArr
for tIdx, token := range tokens {
token = Unescape(token)
if root == nil {
return nil, fmt.Errorf("key '%s' not found:\n%s", token, evalMsg(tIdx, tokens, nil))
}
switch t := root.(type) {
case *Obj:
if val, ok := t.Get(token); ok {
root = val
} else {
root = nil
return nil, fmt.Errorf("key '%s' not found:\n%s", token, evalMsg(tIdx, tokens, t.Keys()))
}
case *Arr:
idx, err := strconv.ParseInt(token, 10, 64)
if err != nil {
return nil, fmt.Errorf("expected integer index:\n%s", evalMsgArr(tIdx, tokens, 0, t.Len()))
}
if idx < 0 || int(idx) >= t.Len() {
return nil, fmt.Errorf("index out of bounds:\n%s", evalMsgArr(tIdx, tokens, 0, t.Len()))
}
root = t.Get(int(idx))
default:
return nil, fmt.Errorf("key '%s' not addressable:\n%s", token, evalMsg(tIdx, tokens, nil))
}
}
return root, nil
}
func MustEval(objOrArr ObjOrArr, ptr Ptr) Value {
v, err := Eval(objOrArr, ptr)
if err != nil {
panic(err)
}
return v
}
func keysAsSlice[T any](m map[string]T) []string {
res := make([]string, len(m))[0:0]
for k := range m {
res = append(res, k)
}
sort.Strings(res)
return res
}
func baseMsg(failedAt int, tokens []PtrToken) *strings.Builder {
tmp := &strings.Builder{}
sb := &strings.Builder{}
for i, t := range tokens {
sb.WriteString("/")
sb.WriteString(t)
if i < failedAt {
for s := 0; s < len(t); s++ {
tmp.WriteString(" ")
}
tmp.WriteString(" ") // for the added /
} else {
if i == failedAt {
tmp.WriteString(" ^")
}
for s := 0; s < len(t); s++ {
tmp.WriteString("~")
}
}
}
sb.WriteString("\n")
sb.WriteString(tmp.String())
return sb
}
// evalMsg generates an over-engineered error message
func evalMsg(failedAt int, tokens []PtrToken, keysInContext []string) string {
sb := baseMsg(failedAt, tokens)
if len(keysInContext) > 0 {
sb.WriteString(" available keys: ")
sb.WriteString("(")
for kid, key := range keysInContext {
sb.WriteString(key)
if kid < len(keysInContext)-1 {
sb.WriteString("|")
}
}
sb.WriteString(")")
}
if keysInContext == nil {
sb.WriteString(" object is nil")
}
return sb.String()
}
// evalMsgArr generates an over-engineered error message
func evalMsgArr(failedAt int, tokens []PtrToken, min, max int) string {
sb := baseMsg(failedAt, tokens)
sb.WriteString(fmt.Sprintf(" index must be in [%d...%d[", min, max))
return sb.String()
}
// Escape takes any string and returns a token.
// ~ becomes ~0 and / becomes ~1
func Escape(str string) PtrToken {
tmp := strings.Replace(str, "~", "~0", -1)
return strings.Replace(tmp, "/", "~1", -1)
}
// Unescape takes a token and returns the original string.
// ~0 becomes ~ and ~1 becomes /
func Unescape(str PtrToken) string {
tmp := strings.Replace(str, "~1", "/", -1)
return strings.Replace(tmp, "~0", "~", -1)
}