Skip to content

Commit dc201c7

Browse files
committed
test: added tests for edge cases
* fixed panic case when attempting to set a non-assignable value * clarified example and limitation regarding embedded structs Signed-off-by: Frederic BIDON <[email protected]>
1 parent 3f0fe76 commit dc201c7

File tree

3 files changed

+229
-14
lines changed

3 files changed

+229
-14
lines changed

pointer.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type JSONSetable interface {
4545
// - a go map[K]V is interpreted as an object, with type K assignable to a string
4646
// - a go slice []T is interpreted as an array
4747
// - a go struct is interpreted as an object, with exported fields interpreted as keys
48+
// - promoted fields from an embedded struct are traversed
4849
// - scalars (e.g. int, float64 ...), channels, functions and go arrays cannot be traversed
4950
//
5051
// For struct s resolved by reflection, key mappings honor the conventional struct tag `json`.
@@ -54,7 +55,7 @@ type JSONSetable interface {
5455
// # Limitations
5556
//
5657
// - Unlike go standard marshaling, untagged fields do not default to the go field name and are ignored.
57-
// - anonymous embedded fields are not traversed
58+
// - anonymous fields are not traversed if untagged
5859
type Pointer struct {
5960
referenceTokens []string
6061
}
@@ -362,7 +363,7 @@ func getSingleImpl(node any, decodedToken string, nameProvider *jsonname.NamePro
362363
case reflect.Slice:
363364
tokenIndex, err := strconv.Atoi(decodedToken)
364365
if err != nil {
365-
return nil, kind, err
366+
return nil, kind, errors.Join(err, ErrPointer)
366367
}
367368
sLength := rValue.Len()
368369
if tokenIndex < 0 || tokenIndex >= sLength {
@@ -396,21 +397,34 @@ func setSingleImpl(node, data any, decodedToken string, nameProvider *jsonname.N
396397
return fmt.Errorf("object has no field %q: %w", decodedToken, ErrPointer)
397398
}
398399
fld := rValue.FieldByName(nm)
399-
if fld.IsValid() {
400-
fld.Set(reflect.ValueOf(data))
400+
if !fld.CanSet() {
401+
return fmt.Errorf("can't set struct field %s to %v: %w", nm, data, ErrPointer)
402+
}
403+
404+
value := reflect.ValueOf(data)
405+
valueType := value.Type()
406+
assignedType := fld.Type()
407+
408+
if !valueType.AssignableTo(assignedType) {
409+
return fmt.Errorf("can't set value with type %T to field %s with type %v: %w", data, nm, assignedType, ErrPointer)
401410
}
411+
412+
fld.Set(value)
413+
402414
return nil
403415

404416
case reflect.Map:
405417
kv := reflect.ValueOf(decodedToken)
406418
rValue.SetMapIndex(kv, reflect.ValueOf(data))
419+
407420
return nil
408421

409422
case reflect.Slice:
410423
tokenIndex, err := strconv.Atoi(decodedToken)
411424
if err != nil {
412-
return err
425+
return errors.Join(err, ErrPointer)
413426
}
427+
414428
sLength := rValue.Len()
415429
if tokenIndex < 0 || tokenIndex >= sLength {
416430
return errOutOfBounds(sLength, tokenIndex)
@@ -420,7 +434,17 @@ func setSingleImpl(node, data any, decodedToken string, nameProvider *jsonname.N
420434
if !elem.CanSet() {
421435
return fmt.Errorf("can't set slice index %s to %v: %w", decodedToken, data, ErrPointer)
422436
}
423-
elem.Set(reflect.ValueOf(data))
437+
438+
value := reflect.ValueOf(data)
439+
valueType := value.Type()
440+
assignedType := elem.Type()
441+
442+
if !valueType.AssignableTo(assignedType) {
443+
return fmt.Errorf("can't set value with type %T to slice element %d with type %v: %w", data, tokenIndex, assignedType, ErrPointer)
444+
}
445+
446+
elem.Set(value)
447+
424448
return nil
425449

426450
default:

pointer_test.go

Lines changed: 173 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package jsonpointer
66
import (
77
"encoding/json"
88
"fmt"
9+
"reflect"
910
"strconv"
1011
"testing"
1112

@@ -246,6 +247,7 @@ func TestPointableInterface(t *testing.T) {
246247

247248
t.Run("with pointable type", func(t *testing.T) {
248249
p := &pointableImpl{"hello"}
250+
249251
result, _, err := GetForToken(p, "some")
250252
require.NoError(t, err)
251253
assert.Equal(t, p.a, result)
@@ -346,6 +348,95 @@ func TestArray(t *testing.T) {
346348
}
347349
}
348350

351+
func TestStruct(t *testing.T) {
352+
t.Parallel()
353+
354+
t.Run("with untagged struct field", func(t *testing.T) {
355+
type Embedded struct {
356+
D int `json:"d"`
357+
}
358+
359+
s := struct {
360+
Embedded
361+
362+
A int `json:"a"`
363+
B int
364+
Anonymous struct {
365+
C int `json:"c"`
366+
}
367+
}{}
368+
369+
{
370+
s.A = 1
371+
s.B = 2
372+
s.Anonymous.C = 3
373+
s.D = 4
374+
}
375+
376+
t.Run(`should resolve field A tagged "a"`, func(t *testing.T) {
377+
pointerA, err := New("/a")
378+
require.NoError(t, err)
379+
380+
value, kind, err := pointerA.Get(s)
381+
require.NoError(t, err)
382+
require.Equal(t, reflect.Int, kind)
383+
require.Equal(t, 1, value)
384+
385+
_, err = pointerA.Set(&s, 9)
386+
require.NoError(t, err)
387+
388+
value, _, err = pointerA.Get(s)
389+
require.NoError(t, err)
390+
require.Equal(t, 9, value)
391+
})
392+
393+
t.Run(`should resolve embedded field D with tag`, func(t *testing.T) {
394+
pointerD, err := New("/d")
395+
require.NoError(t, err)
396+
397+
value, kind, err := pointerD.Get(s)
398+
require.NoError(t, err)
399+
require.Equal(t, reflect.Int, kind)
400+
require.Equal(t, 4, value)
401+
402+
_, err = pointerD.Set(&s, 6)
403+
require.NoError(t, err)
404+
405+
value, _, err = pointerD.Get(s)
406+
require.NoError(t, err)
407+
require.Equal(t, 6, value)
408+
})
409+
410+
t.Run("with known limitations", func(t *testing.T) {
411+
t.Run(`should not resolve field B without tag`, func(t *testing.T) {
412+
pointerB, err := New("/B")
413+
require.NoError(t, err)
414+
415+
_, _, err = pointerB.Get(s)
416+
require.Error(t, err)
417+
require.ErrorContains(t, err, `has no field "B"`)
418+
419+
_, err = pointerB.Set(&s, 8)
420+
require.Error(t, err)
421+
require.ErrorContains(t, err, `has no field "B"`)
422+
})
423+
424+
t.Run(`should not resolve field C with tag, but anonymous`, func(t *testing.T) {
425+
pointerC, err := New("/c")
426+
require.NoError(t, err)
427+
428+
_, _, err = pointerC.Get(s)
429+
require.Error(t, err)
430+
require.ErrorContains(t, err, `has no field "c"`)
431+
432+
_, err = pointerC.Set(&s, 7)
433+
require.Error(t, err)
434+
require.ErrorContains(t, err, `has no field "c"`)
435+
})
436+
})
437+
})
438+
}
439+
349440
func TestOtherThings(t *testing.T) {
350441
t.Parallel()
351442

@@ -367,11 +458,21 @@ func TestOtherThings(t *testing.T) {
367458
})
368459

369460
t.Run("out of bound array index should error", func(t *testing.T) {
370-
p, err := New("/foo/3")
371-
require.NoError(t, err)
461+
t.Run("with index overflow", func(t *testing.T) {
462+
p, err := New("/foo/3")
463+
require.NoError(t, err)
372464

373-
_, _, err = p.Get(testDocumentJSON(t))
374-
require.Error(t, err)
465+
_, _, err = p.Get(testDocumentJSON(t))
466+
require.Error(t, err)
467+
})
468+
469+
t.Run("with index unerflow", func(t *testing.T) {
470+
p, err := New("/foo/-3")
471+
require.NoError(t, err)
472+
473+
_, _, err = p.Get(testDocumentJSON(t))
474+
require.Error(t, err)
475+
})
375476
})
376477

377478
t.Run("referring to a key in an array should error", func(t *testing.T) {
@@ -907,4 +1008,72 @@ func TestEdgeCases(t *testing.T) {
9071008

9081009
require.Equal(t, doc, newDoc)
9091010
})
1011+
1012+
t.Run("with out of bounds index", func(t *testing.T) {
1013+
p, err := New("/foo/10")
1014+
require.NoError(t, err)
1015+
1016+
t.Run("should error on Get", func(t *testing.T) {
1017+
_, _, err := p.Get(testStructJSONDoc(t))
1018+
require.Error(t, err)
1019+
require.ErrorContains(t, err, "index out of bounds")
1020+
})
1021+
1022+
t.Run("should error on Set", func(t *testing.T) {
1023+
_, err := p.Set(testStructJSONPtr(t), "peek-a-boo")
1024+
require.Error(t, err)
1025+
require.ErrorContains(t, err, "index out of bounds")
1026+
})
1027+
})
1028+
1029+
t.Run("Set with invalid pointer token", func(t *testing.T) {
1030+
doc := testStructJSONDoc(t)
1031+
pointer, err := New("/foo/x")
1032+
require.NoError(t, err)
1033+
1034+
_, err = pointer.Set(&doc, "yay")
1035+
require.Error(t, err)
1036+
require.ErrorContains(t, err, `Atoi: parsing "x"`)
1037+
})
1038+
1039+
t.Run("Set with invalid reference in struct", func(t *testing.T) {
1040+
doc := struct {
1041+
A func() `json:"a"`
1042+
B []int `json:"b"`
1043+
}{
1044+
A: func() {},
1045+
B: []int{0, 1},
1046+
}
1047+
1048+
t.Run("should error when attempting to set a struct field value that is not assignable", func(t *testing.T) {
1049+
pointerA, err := New("/a")
1050+
require.NoError(t, err)
1051+
1052+
_, err = pointerA.Set(&doc, "waou")
1053+
require.Error(t, err)
1054+
require.ErrorContains(t, err, `can't set value with type string to field A`)
1055+
})
1056+
1057+
t.Run("should error when attempting to set a slice element value that is not assignable", func(t *testing.T) {
1058+
pointerB, err := New("/b/0")
1059+
require.NoError(t, err)
1060+
1061+
_, err = pointerB.Set(&doc, "waou")
1062+
require.Error(t, err)
1063+
require.ErrorContains(t, err, `can't set value with type string to slice element 0 with type int`)
1064+
})
1065+
1066+
t.Run("should error when attempting to set a value that does not exist", func(t *testing.T) {
1067+
pointerB, err := New("/x")
1068+
require.NoError(t, err)
1069+
1070+
_, _, err = pointerB.Get(&doc)
1071+
require.Error(t, err)
1072+
require.ErrorContains(t, err, `no field`)
1073+
1074+
_, err = pointerB.Set(&doc, "oops")
1075+
require.Error(t, err)
1076+
require.ErrorContains(t, err, `no field`)
1077+
})
1078+
})
9101079
}

struct_example_test.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
var ErrExampleIface = errors.New("example error")
1111

1212
type ExampleDoc struct {
13+
PromotedDoc
14+
1315
Promoted EmbeddedDoc `json:"promoted"`
1416
AnonPromoted EmbeddedDoc `json:"-"`
1517
A string `json:"propA"`
@@ -23,8 +25,15 @@ type EmbeddedDoc struct {
2325
B string `json:"propB"`
2426
}
2527

28+
type PromotedDoc struct {
29+
C string `json:"propC"`
30+
}
31+
2632
func Example_struct() {
2733
doc := ExampleDoc{
34+
PromotedDoc: PromotedDoc{
35+
C: "c",
36+
},
2837
Promoted: EmbeddedDoc{
2938
B: "promoted",
3039
},
@@ -43,15 +52,27 @@ func Example_struct() {
4352
}
4453
fmt.Printf("a: %v\n", a)
4554
}
55+
4656
{
47-
// tagged embedded field is resolved
57+
// tagged struct field is resolved
4858
pointerB, _ := jsonpointer.New("/promoted/propB")
49-
a, _, err := pointerB.Get(doc)
59+
b, _, err := pointerB.Get(doc)
60+
if err != nil {
61+
fmt.Println(err)
62+
return
63+
}
64+
fmt.Printf("b: %v\n", b)
65+
}
66+
67+
{
68+
// tagged embedded field is resolved
69+
pointerC, _ := jsonpointer.New("/propC")
70+
c, _, err := pointerC.Get(doc)
5071
if err != nil {
5172
fmt.Println(err)
5273
return
5374
}
54-
fmt.Printf("b: %v\n", a)
75+
fmt.Printf("c: %v\n", c)
5576
}
5677

5778
{
@@ -69,7 +90,7 @@ func Example_struct() {
6990
}
7091

7192
{
72-
// Limitation: anonymous embedded field is not resolved.
93+
// Limitation: anonymous field is not resolved.
7394
pointerC, _ := jsonpointer.New("/propB")
7495
_, _, err := pointerC.Get(doc)
7596
fmt.Printf("anonymous: %v\n", err)
@@ -85,6 +106,7 @@ func Example_struct() {
85106
// output:
86107
// a: a
87108
// b: promoted
109+
// c: c
88110
// ignored: object has no field "ignored": JSON pointer error
89111
// unexported: object has no field "unexported": JSON pointer error
90112
// anonymous: object has no field "propB": JSON pointer error

0 commit comments

Comments
 (0)