Skip to content

Commit 6ac320b

Browse files
committedJul 1, 2023
Add relative json pointers.
1 parent 815b366 commit 6ac320b

File tree

8 files changed

+267
-22
lines changed

8 files changed

+267
-22
lines changed
 

‎.github/workflows/ci.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
on: [ push ]
2+
jobs:
3+
test:
4+
runs-on: ubuntu-latest
5+
steps:
6+
- uses: actions/checkout@v3
7+
- uses: actions/setup-go@v4
8+
with:
9+
go-version: '1.20'
10+
- run: go test -v ./...

‎README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
# (Relative) JSON Pointers
22

33
- [RFC6901 - JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901)
4+
- [Relative JSON Pointers](https://www.ietf.org/archive/id/draft-hha-relative-json-pointer-00.html)

‎abnf/ir/ir.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ir
33
import (
44
"fmt"
55
"github.com/0x51-dev/upeg/parser"
6+
"strconv"
67
"strings"
78
)
89

@@ -24,3 +25,56 @@ func ParseJsonPointer(n *parser.Node) ([]string, error) {
2425
}
2526
return tokens, nil
2627
}
28+
29+
type RelativeJsonPointer struct {
30+
NonNegativeInteger int
31+
IndexManipulation *int
32+
JsonPointer *[]string
33+
}
34+
35+
func ParseRelativeJsonPointer(n *parser.Node) (*RelativeJsonPointer, error) {
36+
if n.Name != "RelativeJsonPointer" {
37+
return nil, fmt.Errorf("expected RelativeJsonPointer, got %s", n.Name)
38+
}
39+
var ptr RelativeJsonPointer
40+
for _, n := range n.Children() {
41+
switch n.Name {
42+
case "JsonPointer":
43+
v, err := ParseJsonPointer(n)
44+
if err != nil {
45+
return nil, err
46+
}
47+
ptr.JsonPointer = &v
48+
case "OriginSpecification":
49+
for _, n := range n.Children() {
50+
switch n.Name {
51+
case "NonNegativeInteger":
52+
v, err := strconv.Atoi(n.Value())
53+
if err != nil {
54+
return nil, err
55+
}
56+
ptr.NonNegativeInteger = v
57+
case "IndexManipulation":
58+
str := n.Value()
59+
v, err := strconv.Atoi(str[1:])
60+
if err != nil {
61+
return nil, err
62+
}
63+
switch str[0] {
64+
case '+':
65+
case '-':
66+
v = -v
67+
default:
68+
return nil, fmt.Errorf("expected + or -, got %c", str[0])
69+
}
70+
ptr.IndexManipulation = &v
71+
default:
72+
return nil, fmt.Errorf("expected NonNegativeInteger or IndexManipulation, got %s", n.Name)
73+
}
74+
}
75+
default:
76+
return nil, fmt.Errorf("expected JsonPointer or OriginSpecification, got %s", n.Name)
77+
}
78+
}
79+
return &ptr, nil
80+
}

‎abnf/jsonptr.abnf

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ unescaped = %x00-2E / %x30-7D / %x7F-FFFC / %xFFFE-10FFFF
44
; %x2F ('/') and %x7E ('~') are excluded from 'unescaped'
55
escaped = "~" ( "0" / "1" )
66
; representing '~' and '/', respectively
7-
relative-json-pointer = non-negative-integer ([index-manipulation] json-pointer / "#")
8-
index-manipulation = ("+" / "-") non-negative-integer
9-
non-negative-integer = %x30 / %x31-39 *( %x30-39 )
10-
; "0", or digits without a leading "0"
7+
8+
relative-json-pointer = origin-specification ( "#" / json-pointer )
9+
; json-pointer from RFC 6901
10+
origin-specification = non-negative-integer [ index-manipulation ]
11+
index-manipulation = ( "+" / "-" ) positive-integer
12+
non-negative-integer = "0" / positive-integer
13+
positive-integer = %x31-39 *%x30-39
14+
; digits without a leading zero

‎abnf/jsonptr.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ var (
1010
ReferenceToken = op.Capture{Name: "ReferenceToken", Value: op.ZeroOrMore{Value: op.Or{Unescaped, Escaped}}}
1111
Unescaped = op.Or{op.RuneRange{Min: 0x00, Max: 0x2E}, op.RuneRange{Min: 0x30, Max: 0x7D}, op.RuneRange{Min: 0x7F, Max: 0xFFFC}, op.RuneRange{Min: 0xFFFE, Max: 0x10FFFF}}
1212
Escaped = op.And{'~', op.Or{'0', '1'}}
13-
RelativeJsonPointer = op.Capture{Name: "RelativeJsonPointer", Value: op.And{NonNegativeInteger, op.Or{op.And{op.Optional{Value: IndexManipulation}, JsonPointer}, '#'}}}
14-
IndexManipulation = op.Capture{Name: "IndexManipulation", Value: op.And{op.Or{'+', '-'}, NonNegativeInteger}}
15-
NonNegativeInteger = op.Capture{Name: "NonNegativeInteger", Value: op.Or{rune(0x30), op.And{op.RuneRange{Min: 0x31, Max: 0x39}, op.ZeroOrMore{Value: op.RuneRange{Min: 0x30, Max: 0x39}}}}}
13+
RelativeJsonPointer = op.Capture{Name: "RelativeJsonPointer", Value: op.And{OriginSpecification, op.Or{'#', JsonPointer}}}
14+
OriginSpecification = op.Capture{Name: "OriginSpecification", Value: op.And{NonNegativeInteger, op.Optional{Value: IndexManipulation}}}
15+
IndexManipulation = op.Capture{Name: "IndexManipulation", Value: op.And{op.Or{'+', '-'}, PositiveInteger}}
16+
NonNegativeInteger = op.Capture{Name: "NonNegativeInteger", Value: op.Or{'0', PositiveInteger}}
17+
PositiveInteger = op.And{op.RuneRange{Min: 0x31, Max: 0x39}, op.ZeroOrMore{Value: op.RuneRange{Min: 0x30, Max: 0x39}}}
1618
)

‎abnf/jsonptr_test.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"testing"
99
)
1010

11-
func TestRelativeJsonPointer(t *testing.T) {
11+
func TestJsonPointer(t *testing.T) {
1212
for _, test := range []struct {
1313
ptr string
1414
expected []string
@@ -47,3 +47,27 @@ func TestRelativeJsonPointer(t *testing.T) {
4747
}
4848
}
4949
}
50+
51+
func TestRelativePointer(t *testing.T) {
52+
for _, test := range []string{
53+
"0",
54+
"1/0",
55+
"0-1",
56+
"2/highly/nested/objects",
57+
"0#",
58+
"0-1#",
59+
"1#",
60+
} {
61+
p, err := parser.New([]rune(test))
62+
if err != nil {
63+
t.Fatal(err)
64+
}
65+
n, err := p.Parse(op.And{abnf.RelativeJsonPointer, op.EOF{}})
66+
if err != nil {
67+
t.Fatal(test, err)
68+
}
69+
if _, err := ir.ParseRelativeJsonPointer(n); err != nil {
70+
t.Fatal(err)
71+
}
72+
}
73+
}

‎jsonptr.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,79 @@ func (ptr JsonPointer) evalMap(document map[string]any) (any, error) {
6969
}
7070
return ptr[1:].evalAny(v)
7171
}
72+
73+
type RelativeJsonPointer ir.RelativeJsonPointer
74+
75+
func ParseRelativeJsonPointer(ptr string) (*RelativeJsonPointer, error) {
76+
p, err := parser.New([]rune(ptr))
77+
if err != nil {
78+
return nil, err
79+
}
80+
n, err := p.Parse(op.And{abnf.RelativeJsonPointer, op.EOF{}})
81+
if err != nil {
82+
return nil, err
83+
}
84+
r, err := ir.ParseRelativeJsonPointer(n)
85+
if err != nil {
86+
return nil, err
87+
}
88+
return (*RelativeJsonPointer)(r), nil
89+
}
90+
91+
func (ptr RelativeJsonPointer) Eval(start JsonPointer, document map[string]any) (any, error) {
92+
current := start[:]
93+
94+
// 1. Processing the non-negative-integer prefix.
95+
switch ptr.NonNegativeInteger {
96+
case 0: // Skip!
97+
default:
98+
for i := 0; i < ptr.NonNegativeInteger; i++ {
99+
if len(current) == 0 {
100+
// If the current referenced value is the root of the document, then evaluation fails.
101+
return nil, fmt.Errorf("referencing root document")
102+
}
103+
// If the referenced value is an item within an array/object, then the new referenced value is that array/object.
104+
current = current[:len(current)-1]
105+
}
106+
}
107+
108+
// 2. Processing the index-manipulation suffix.
109+
if ptr.IndexManipulation != nil {
110+
v, err := strconv.Atoi(current[len(current)-1])
111+
if err != nil {
112+
return nil, fmt.Errorf("referencing non-integer key")
113+
}
114+
v += *ptr.IndexManipulation
115+
current[len(current)-1] = strconv.Itoa(v)
116+
}
117+
118+
// 3. Processing the JSON Pointer suffix.
119+
if ptr.JsonPointer != nil {
120+
current = append(current, *ptr.JsonPointer...)
121+
} else {
122+
// The remainder of the Relative JSON Pointer is the character '#'.
123+
if len(current) == 0 {
124+
// If the current referenced value is the root of the document, then evaluation fails.
125+
return nil, fmt.Errorf("referencing root document")
126+
}
127+
p, err := current[:len(current)-1].Eval(document)
128+
if err != nil {
129+
return nil, err
130+
}
131+
switch p.(type) {
132+
case nil, map[string]any:
133+
return current[len(current)-1], nil
134+
case []any:
135+
v := current[len(current)-1]
136+
i, err := strconv.Atoi(v)
137+
if err != nil {
138+
return nil, fmt.Errorf("referencing non-integer key")
139+
}
140+
return float64(i), nil
141+
default:
142+
return nil, fmt.Errorf("referencing non-object, non-array")
143+
}
144+
}
145+
146+
return current.Eval(document)
147+
}

‎jsonptr_test.go

Lines changed: 88 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,23 +47,97 @@ func TestJsonPointer_Eval(t *testing.T) {
4747
if err != nil {
4848
t.Fatal(err)
4949
}
50-
switch v := test.val.(type) {
51-
case []any:
52-
for i, v := range v {
53-
if v != val.([]any)[i] {
54-
t.Fatalf("expected %v, got %v", test.val, val)
55-
}
50+
cmp(t, test.val, val)
51+
}
52+
}
53+
54+
func TestRelativeJsonPointer_Eval(t *testing.T) {
55+
documentStr := `{
56+
"foo": ["bar", "baz", "biz"],
57+
"highly": {
58+
"nested": {
59+
"objects": true
5660
}
57-
default:
58-
switch v := test.val.(type) {
59-
case string:
60-
if val != v {
61-
t.Fatalf("expected %v, got %v", test.val, val)
61+
}
62+
}`
63+
var document map[string]any
64+
if err := json.Unmarshal([]byte(documentStr), &document); err != nil {
65+
t.Fatal(err)
66+
}
67+
for _, test := range []struct {
68+
path string
69+
tests []struct {
70+
ptr string
71+
val any
72+
}
73+
}{
74+
{
75+
path: "/foo/1",
76+
tests: []struct {
77+
ptr string
78+
val any
79+
}{
80+
{"0", "baz"},
81+
{"1/0", "bar"},
82+
{"0-1", "bar"},
83+
{"2/highly/nested/objects", true},
84+
{"0#", 1},
85+
{"0+1#", 2},
86+
{"1#", "foo"},
87+
},
88+
},
89+
{
90+
path: "/highly/nested",
91+
tests: []struct {
92+
ptr string
93+
val any
94+
}{
95+
{"0/objects", true},
96+
{"1/nested/objects", true},
97+
{"2/foo/0", "bar"},
98+
{"0#", "nested"},
99+
{"1#", "highly"},
100+
},
101+
},
102+
} {
103+
t.Run(test.path, func(t *testing.T) {
104+
path := test.path
105+
for _, test := range test.tests {
106+
start, err := jsonptr.ParseJsonPointer(path)
107+
if err != nil {
108+
t.Fatal(err)
62109
}
63-
case int:
64-
if val != float64(v) { // JSON numbers are always float64.
65-
t.Fatalf("expected %v, got %v", test.val, val)
110+
ptr, err := jsonptr.ParseRelativeJsonPointer(test.ptr)
111+
if err != nil {
112+
t.Fatal(err)
66113
}
114+
val, err := ptr.Eval(start, document)
115+
if err != nil {
116+
t.Fatal(err)
117+
}
118+
cmp(t, test.val, val)
119+
}
120+
})
121+
}
122+
}
123+
124+
func cmp(t *testing.T, a, b any) {
125+
switch a := a.(type) {
126+
case []any:
127+
for i, a := range a {
128+
if a != b.([]any)[i] {
129+
t.Fatalf("expected %v, got %v", a, b)
130+
}
131+
}
132+
default:
133+
switch a := a.(type) {
134+
case string:
135+
if b != a {
136+
t.Fatalf("expected %v, got %v", a, b)
137+
}
138+
case int:
139+
if b != float64(a) { // JSON numbers are always float64.
140+
t.Fatalf("expected %v, got %v", a, b)
67141
}
68142
}
69143
}

0 commit comments

Comments
 (0)
Please sign in to comment.