Skip to content

Commit 305b70b

Browse files
committed
feat: can validate a jsonpointer str + get the corresponding value in a JSON object
1 parent 7a4bfb6 commit 305b70b

File tree

2 files changed

+181
-0
lines changed

2 files changed

+181
-0
lines changed

pointer.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package jsonpointergo
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
)
8+
9+
const (
10+
JSONPointerEmptyPointer = ""
11+
JSONPointerSeparatorToken = "/"
12+
JSONPointerEscapeToken = "~"
13+
JSONPointerSlashEncoded = "~1"
14+
JSONPointerTildaEncoded = "~0"
15+
)
16+
17+
type JSONObject = map[string]any
18+
19+
type JSONPointer struct {
20+
referenceTokens []string
21+
}
22+
23+
func NewJSONPointer(jsonPointer string) (*JSONPointer, error) {
24+
tokens, err := parseJSONPointerString(jsonPointer)
25+
if err != nil {
26+
return nil, err
27+
}
28+
return &JSONPointer{
29+
referenceTokens: tokens,
30+
}, nil
31+
}
32+
33+
func parseJSONPointerString(jsonPointer string) ([]string, error) {
34+
if jsonPointer == JSONPointerEmptyPointer {
35+
return nil, fmt.Errorf(
36+
"jsonpointer: the jsonpointer is empty",
37+
)
38+
}
39+
if !strings.HasPrefix(jsonPointer, JSONPointerSeparatorToken) {
40+
return nil, fmt.Errorf(
41+
"jsonpointer: a jsonpointer should start with a reference to the root value: %v",
42+
JSONPointerSeparatorToken,
43+
)
44+
}
45+
tokens := strings.Split(jsonPointer, JSONPointerSeparatorToken)
46+
return tokens[1:], nil
47+
}
48+
49+
func (jp *JSONPointer) GetValue(document JSONObject) (any, error) {
50+
if document == nil {
51+
return nil, fmt.Errorf(
52+
"jsonpointer: the JSON document provided is nil",
53+
)
54+
}
55+
var subDocument any
56+
subDocument = document
57+
for i, tokenRefEncoded := range jp.referenceTokens {
58+
tokenRef := decodeJSONPointerReference(tokenRefEncoded)
59+
jsonDoc, ok := subDocument.(JSONObject)
60+
if ok {
61+
value, ok := jsonDoc[tokenRef]
62+
if !ok {
63+
return nil, fmt.Errorf(
64+
"jsonpointer: the document provided does not have the following reference: %v, %v",
65+
tokenRef, i,
66+
)
67+
}
68+
subDocument = value
69+
continue
70+
}
71+
jsonArray, ok := subDocument.([]any)
72+
if ok {
73+
index, err := strconv.Atoi(tokenRef)
74+
if err != nil {
75+
return nil, fmt.Errorf(
76+
"jsonpointer: the reference is trying to access a field on an array: %v",
77+
tokenRef,
78+
)
79+
}
80+
if index < 0 || index >= len(jsonArray) {
81+
return nil, fmt.Errorf(
82+
"jsonpointer: the index provided [%v] is trying to access an out of bond item on an array of length %v",
83+
index,
84+
len(jsonArray),
85+
)
86+
}
87+
subDocument = jsonArray[index]
88+
continue
89+
}
90+
return nil, fmt.Errorf("jsonpointer: the reference is trying to access a single value: %v. Type of subdocument: %T", tokenRef, subDocument)
91+
}
92+
return subDocument, nil
93+
}
94+
95+
func decodeJSONPointerReference(ref string) string {
96+
refWithSlash := strings.ReplaceAll(
97+
ref,
98+
JSONPointerSlashEncoded,
99+
JSONPointerSeparatorToken,
100+
)
101+
return strings.ReplaceAll(
102+
refWithSlash,
103+
JSONPointerTildaEncoded,
104+
JSONPointerEscapeToken,
105+
)
106+
}

pointer_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package jsonpointergo
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestNewJSONPointer(t *testing.T) {
8+
tests := []struct {
9+
jsonPointer string
10+
wantErr bool
11+
}{
12+
{"/foo/bar", false},
13+
{"", true},
14+
{"foo/bar", true},
15+
}
16+
17+
for _, test := range tests {
18+
_, err := NewJSONPointer(test.jsonPointer)
19+
if (err != nil) != test.wantErr {
20+
t.Errorf("NewJSONPointer(%v) error = %v, wantErr %v", test.jsonPointer, err, test.wantErr)
21+
}
22+
}
23+
}
24+
25+
func TestGetValue(t *testing.T) {
26+
document := map[string]any{
27+
"foo": map[string]any{
28+
"bar": "baz",
29+
},
30+
"array": []any{1, 2, 3},
31+
}
32+
33+
tests := []struct {
34+
jsonPointer string
35+
want any
36+
wantErr bool
37+
}{
38+
{"/foo/bar", "baz", false},
39+
{"/array/0", 1, false},
40+
{"/array/3", nil, true},
41+
{"/nonexistent", nil, true},
42+
}
43+
44+
for _, test := range tests {
45+
jp, err := NewJSONPointer(test.jsonPointer)
46+
if err != nil {
47+
t.Fatalf("NewJSONPointer(%v) error = %v", test.jsonPointer, err)
48+
}
49+
got, err := jp.GetValue(document)
50+
if (err != nil) != test.wantErr {
51+
t.Errorf("JSONPointer.GetValue() error = %v, wantErr %v", err, test.wantErr)
52+
continue
53+
}
54+
if got != test.want {
55+
t.Errorf("JSONPointer.GetValue() = %v, want %v", got, test.want)
56+
}
57+
}
58+
}
59+
60+
func TestDecodeJSONPointerReference(t *testing.T) {
61+
tests := []struct {
62+
ref string
63+
want string
64+
}{
65+
{"~1", "/"},
66+
{"~0", "~"},
67+
{"~01", "~1"},
68+
}
69+
70+
for _, test := range tests {
71+
if got := decodeJSONPointerReference(test.ref); got != test.want {
72+
t.Errorf("decodeJSONPointerReference(%v) = %v, want %v", test.ref, got, test.want)
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)