Skip to content

Commit f59a16a

Browse files
committed
finishes up delete + adds examples
1 parent 3ceb3c8 commit f59a16a

File tree

11 files changed

+412
-78
lines changed

11 files changed

+412
-78
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ jsonpointer was built to support
1010
[github.com/chanced/openapi](https://github.com/chanced/openapi) but it may be
1111
useful for others so it has been released as an independent package.
1212

13+
For the openapi package, I needed a way to resolve and assign JSON Pointers
14+
against concrete types while also maintaining integrity of pointer values. All
15+
existing JSON Pointer implementations for Go operate on `map[string]interface{}`
16+
and `[]interface{}`, raw JSON, or both.
17+
1318
## Install
1419

1520
```bash
@@ -119,7 +124,7 @@ All methods return new values rather than modifying the pointer itself. If you w
119124
func (mt MyType) ResolveJSONPointer(ptr *jsonpointer.JSONPointer, op Operation) (interface{}, error) {
120125
next, t, ok := ptr.Next()
121126
if !ok {
122-
// this will only occur if the ptr is a root token
127+
// this will only occur if the ptr is a root token in this circumstance
123128
return mt
124129
}
125130
if op == jsonpointer.Assigning && t == "someInterface" {

assign.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,27 @@ import (
1717
"reflect"
1818
)
1919

20+
// Assigner is the interface implemented by types which can assign values via
21+
// JSON Pointers. The input can be assumed to be a valid JSON Pointer and the
22+
// value to assign.
23+
//
24+
// AssignByJSONPointer is called after the value has been resolved. If custom
25+
// resolution is needed, the type should also implement Resolver.
26+
//
2027
type Assigner interface {
2128
AssignByJSONPointer(ptr *JSONPointer, value interface{}) error
2229
}
2330

31+
// Assign performs an assignment of value to the target dst specified by the
32+
// JSON Pointer ptr. Assign traverses dst recursively, resolving the path of
33+
// the JSON Pointer. If a type in the path implements Resolver, it will attempt
34+
// to resolve by invoking ResolveJSONPointer on that value. If ResolveJSONPointer
35+
// returns YieldOperation or if the value does not implement ResolveJSONPointer,
36+
// encoding/json naming conventions are utilized to resolve the path.
37+
//
38+
// If a type in the path implements Assigner, AssignByJSONPointer will be called
39+
// with the updated value pertinent to that path.
40+
//
2441
func Assign(dst interface{}, ptr JSONPointer, value interface{}) error {
2542
if err := ptr.Validate(); err != nil {
2643
return err

assign_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@ func TestAssign(t *testing.T) {
3333
value interface{}
3434
run func(v interface{}, err error)
3535
}{
36-
{"/nested/str", "strval", func(val interface{}, err error) {
37-
assert.NoError(err)
38-
assert.Equal(val, r.Nested.Str)
39-
}},
40-
{"/nestedptr/str", "x", func(val interface{}, err error) {
41-
assert.NoError(err)
42-
assert.Equal(val, r.NestedPtr.Str)
43-
}},
36+
// {"/nested/str", "strval", func(val interface{}, err error) {
37+
// assert.NoError(err)
38+
// assert.Equal(val, r.Nested.Str)
39+
// }},
40+
// {"/nestedptr/str", "x", func(val interface{}, err error) {
41+
// assert.NoError(err)
42+
// assert.Equal(val, r.NestedPtr.Str)
43+
// }},
4444
{"/nested/entrymap/keyval/name", "entry-name", func(v interface{}, err error) {
4545
assert.NoError(err)
4646
assert.Contains(r.Nested.EntryMap, "keyval")
@@ -165,7 +165,7 @@ func TestAssignAny(t *testing.T) {
165165
}
166166

167167
for i, test := range tests {
168-
fmt.Printf("=== RUN TestAssignAny #%d, pointer %s\n", i, test.ptr)
168+
fmt.Printf("=== RUN TestAssignAny #%d, pointer %s\n", i+1, test.ptr)
169169
err := jsonpointer.Assign(&m, test.ptr, test.value)
170170
if test.err != nil {
171171
assert.ErrorIs(err, test.err)
@@ -204,7 +204,7 @@ func TestAssignJSON(t *testing.T) {
204204
}
205205

206206
for i, test := range tests {
207-
fmt.Printf("=== RUN TestAssignJSON #%d, pointer %s\n", i, test.ptr)
207+
fmt.Printf("=== RUN TestAssignJSON #%d, pointer %s\n", i+1, test.ptr)
208208
b := []byte(test.json)
209209
err := jsonpointer.Assign(&b, test.ptr, test.value)
210210
var r Root

delete.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,16 @@ type Deleter interface {
1919
DeleteByJSONPointer(ptr *JSONPointer) error
2020
}
2121

22+
// Delete deletes the value at the given JSON pointer from src.
23+
//
24+
// If any part of the path is unreachable, the Delete function is
25+
// considered a success as the value is not present to delete.
2226
func Delete(src interface{}, ptr JSONPointer) error {
2327
if err := ptr.Validate(); err != nil {
2428
return err
2529
}
2630
dv := reflect.ValueOf(src)
27-
s := newState(ptr, Assigning)
31+
s := newState(ptr, Deleting)
2832
defer s.Release()
2933
if dv.Kind() != reflect.Ptr || dv.IsNil() {
3034
return &ptrError{

delete_test.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package jsonpointer_test
1515

1616
import (
17+
"encoding/json"
1718
"fmt"
1819
"testing"
1920

@@ -29,14 +30,67 @@ func TestDelete(t *testing.T) {
2930
root Root
3031
run func(r Root, err error)
3132
}{
32-
{"/nested/str", Root{Nested: Nested{Str: "str val"}}, func(r Root, err error) {
33+
{"/nested/str", Root{Nested: Nested{Str: "str val", Int: 5}}, func(r Root, err error) {
3334
assert.NoError(err)
3435
assert.Equal("", r.Nested.Str)
36+
assert.Equal(5, r.Nested.Int)
37+
}},
38+
{"/nested", Root{Nested: Nested{Str: "str val", Int: 5}}, func(r Root, err error) {
39+
assert.NoError(err)
40+
assert.Equal("", r.Nested.Str)
41+
assert.Equal(0, r.Nested.Int)
42+
}},
43+
{"/nested/deleter/key", Root{Nested: Nested{Deleter: DeleterImpl{Values: map[string]string{"key": "value"}}}}, func(r Root, err error) {
44+
assert.NotContains(r.Nested.Deleter.Values, "key")
45+
assert.NoError(err)
3546
}},
3647
}
48+
3749
for i, test := range tests {
3850
fmt.Printf("=== RUN TestDelete #%d, pointer %s\n", i, test.ptr)
3951
err := jsonpointer.Delete(&test.root, test.ptr)
4052
test.run(test.root, err)
53+
54+
}
55+
}
56+
57+
func TestDeleteJSON(t *testing.T) {
58+
assert := require.New(t)
59+
60+
tests := []struct {
61+
ptr jsonpointer.JSONPointer
62+
root Root
63+
run func(r Root, err error)
64+
}{
65+
{"/nested/str", Root{Nested: Nested{Str: "string", Int: 5}}, func(r Root, err error) {
66+
assert.NoError(err)
67+
assert.Equal("", r.Nested.Str)
68+
assert.Equal(5, r.Nested.Int)
69+
}},
70+
{"/nested", Root{Nested: Nested{Str: "str val", Int: 5}}, func(r Root, err error) {
71+
assert.NoError(err)
72+
assert.Equal("", r.Nested.Str)
73+
assert.Equal(0, r.Nested.Int)
74+
}},
75+
76+
{"/nested/strslice/1", Root{Nested: Nested{StrSlice: []string{"0", "1", "2"}}}, func(r Root, err error) {
77+
assert.NoError(err)
78+
assert.Len(r.Nested.StrSlice, 2)
79+
assert.Equal("2", r.Nested.StrSlice[1])
80+
}},
81+
}
82+
83+
for i, test := range tests {
84+
fmt.Printf("=== RUN TestDelete #%d, pointer %s\n", i, test.ptr)
85+
b, err := json.Marshal(test.root)
86+
fmt.Println(string(b))
87+
88+
assert.NoError(err)
89+
err = jsonpointer.Delete(&b, test.ptr)
90+
var r Root
91+
uerr := json.Unmarshal(b, &r)
92+
assert.NoError(uerr)
93+
fmt.Println(string(b))
94+
test.run(r, err)
4195
}
4296
}

errors.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,13 @@ var (
2727
// This error is returned by JSONPointer.Validate() which is called by
2828
// Resolve, Assign, and Delete.
2929
//
30-
ErrMalformedToken = fmt.Errorf(`jsonpointer: reference must be empty or start with a "/"`)
30+
ErrMalformedToken = errors.New(`jsonpointer: fragment is malformed`)
31+
32+
// ErrMalformedStart is an ErrMalformedToken that is returned when the JSON
33+
// Pointer is not empty or does not start with a "/".
34+
ErrMalformedStart = fmt.Errorf(`%w; pointer must be an empty string or start with "/"`, ErrMalformedToken)
35+
36+
ErrMalformedEncoding = fmt.Errorf("%w; '~' must be encoded as ~0", ErrMalformedToken)
3137

3238
// ErrNonPointer indicates a non-pointer value was passed to Assign.
3339
//

examples_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package jsonpointer_test
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/chanced/jsonpointer"
8+
)
9+
10+
func ExampleNew() {
11+
ptr := jsonpointer.New("foo", "bar") // => "/foo/bar"
12+
fmt.Println(`"` + ptr + `"`)
13+
14+
ptr = jsonpointer.New("foo/bar") // => "/foo~1bar"
15+
fmt.Println(`"` + ptr + `"`)
16+
17+
ptr = jsonpointer.New() // => ""
18+
fmt.Println(`"` + ptr + `"`)
19+
20+
ptr = jsonpointer.New("") // => "/"
21+
fmt.Println(`"` + ptr + `"`)
22+
23+
ptr = jsonpointer.New("/") // => "/~1"
24+
fmt.Println(`"` + ptr + `"`)
25+
26+
ptr = jsonpointer.New("~") // => "/~0"
27+
fmt.Println(`"` + ptr + `"`)
28+
29+
ptr = jsonpointer.New("#/foo/bar") // => "/#~1foo~1bar"
30+
fmt.Println(`"` + ptr + `"`)
31+
32+
// Output:
33+
// "/foo/bar"
34+
// "/foo~1bar"
35+
// ""
36+
// "/"
37+
// "/~1"
38+
// "/~0"
39+
// "/#~1foo~1bar"
40+
}
41+
42+
func ExampleAssign() {
43+
type Bar struct {
44+
Baz string `json:"baz"`
45+
}
46+
type Foo struct {
47+
Bar Bar `json:"bar"`
48+
}
49+
var foo Foo
50+
jsonpointer.Assign(&foo, "/bar/baz", "qux")
51+
fmt.Println(foo.Bar.Baz)
52+
53+
// Assigning JSON by JSONPointer
54+
55+
foo.Bar.Baz = "quux"
56+
b, _ := json.Marshal(foo)
57+
jsonpointer.Assign(&b, "/bar/baz", "corge")
58+
fmt.Println(string(b))
59+
60+
//Output: qux
61+
//{"bar":{"baz":"corge"}}
62+
}
63+
64+
func ExampleResolve() {
65+
type Bar struct {
66+
Baz string `json:"baz"`
67+
}
68+
type Foo struct {
69+
Bar Bar `json:"bar,omitempty"`
70+
}
71+
foo := Foo{Bar{Baz: "qux"}}
72+
73+
var s string
74+
jsonpointer.Resolve(foo, "/bar/baz", &s)
75+
fmt.Println(s)
76+
77+
// Resolving JSON by JSONPointer
78+
79+
b, _ := json.Marshal(foo)
80+
jsonpointer.Resolve(b, "/bar/baz", &s)
81+
fmt.Println(s)
82+
83+
// Output: qux
84+
// qux
85+
}
86+
87+
func ExampleDelete() {
88+
type Bar struct {
89+
Baz string `json:"baz,omitempty"`
90+
}
91+
type Foo struct {
92+
Bar Bar `json:"bar"`
93+
}
94+
foo := Foo{Bar{Baz: "qux"}}
95+
96+
jsonpointer.Delete(foo, "/bar/baz")
97+
fmt.Printf("foo.Bar.Baz: %v\n", foo.Bar.Baz)
98+
99+
// Deleting JSON by JSONPointer
100+
foo.Bar.Baz = "quux"
101+
b, _ := json.Marshal(foo)
102+
jsonpointer.Delete(&b, "/bar/baz")
103+
fmt.Println(string(b))
104+
105+
// Output: foo.Bar.Baz: qux
106+
// {"bar":{}}
107+
}

jsonpointer.go

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ const (
4242
// jsonpointer.New("") => "/"
4343
// jsonpointer.New("/") => "/~1"
4444
// jsonpointer.New("~") => "/~0"
45-
// jsonpointer.New("\\#") => "/#"
4645
//
4746
func New(tokens ...string) JSONPointer {
4847
return NewFromStrings(tokens)
@@ -58,12 +57,6 @@ func NewFromStrings(tokens []string) JSONPointer {
5857
if len(tokens) == 0 {
5958
return ""
6059
}
61-
switch tokens[0] {
62-
case "#":
63-
tokens = tokens[1:]
64-
case "\\#":
65-
tokens[0] = "#"
66-
}
6760
for _, token := range tokens {
6861
b.WriteRune('/')
6962
if _, err := encoder.WriteString(b, token); err != nil {
@@ -114,19 +107,28 @@ func (p JSONPointer) PrependString(token string) JSONPointer {
114107
return p.Prepend(Token(encoder.Replace(token)))
115108
}
116109

117-
func (p JSONPointer) Validate() error {
118-
if err := p.validateStart(); err != nil {
110+
func (p JSONPointer) Validate() (err error) {
111+
if err = p.validateStart(); err != nil {
119112
return err
120113
}
114+
return p.validateeEncoding()
115+
}
116+
117+
func (p JSONPointer) validateeEncoding() error {
118+
if len(p) == 0 {
119+
return nil
120+
}
121+
for i := len(p) - 1; i >= 0; i-- {
122+
if p[i] == '~' && (i == len(p)-1 || (p[i+1] != '0' && p[i+1] != '1')) {
123+
return ErrMalformedEncoding
124+
}
125+
}
121126
return nil
122127
}
123128

124129
func (p JSONPointer) validateStart() error {
125-
if !startsWithSlash(p) {
126-
return &ptrError{
127-
err: ErrMalformedToken,
128-
state: *newState(p, 0),
129-
}
130+
if len(p) > 0 && !startsWithSlash(p) {
131+
return ErrMalformedStart
130132
}
131133
return nil
132134
}
@@ -198,6 +200,9 @@ func lastSlash(ptr JSONPointer) int {
198200
}
199201

200202
func startsWithSlash(ptr JSONPointer) bool {
203+
if len(ptr) == 0 {
204+
return false
205+
}
201206
return ptr[0] == '/'
202207
}
203208

0 commit comments

Comments
 (0)