Skip to content

Commit ee8de9f

Browse files
fix: more robust support for navigating custom structures via jsonpointer
1 parent 0c3c5da commit ee8de9f

File tree

4 files changed

+223
-22
lines changed

4 files changed

+223
-22
lines changed

jsonpointer/jsonpointer.go

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ import (
1010
)
1111

1212
const (
13-
ErrNotFound = errors.Error("not found")
13+
// ErrNotFound is returned when the target is not found.
14+
ErrNotFound = errors.Error("not found")
15+
// ErrInvalidPath is returned when the path is invalid.
1416
ErrInvalidPath = errors.Error("invalid path")
15-
ErrValidation = errors.Error("validation error")
17+
// ErrValidation is returned when the jsonpointer is invalid.
18+
ErrValidation = errors.Error("validation error")
19+
// ErrSkipInterface is returned when this implementation of the interface is not applicable to the current type.
20+
ErrSkipInterface = errors.Error("skip interface")
1621
)
1722

1823
const (
@@ -171,26 +176,41 @@ type IndexNavigable interface {
171176

172177
// NavigableNoder is an interface that can be implemented by a struct to allow returning an alternative node to evaluate instead of the struct itself.
173178
type NavigableNoder interface {
174-
GetNavigableNode() any
179+
GetNavigableNode() (any, error)
175180
}
176181

177182
func getStructTarget(sourceVal reflect.Value, currentPart navigationPart, stack []navigationPart, currentPath string, o *options) (any, []navigationPart, error) {
178-
sourceValElem := reflect.Indirect(sourceVal)
179-
180-
if currentPart.Type != partTypeKey {
181-
return nil, nil, ErrInvalidPath.Wrap(fmt.Errorf("expected key, got %s at %s", currentPart.Type, currentPath))
182-
}
183-
184-
if sourceVal.Type().Implements(reflect.TypeOf((*KeyNavigable)(nil)).Elem()) {
185-
return getNavigableWithKeyTarget(sourceVal, currentPart, stack, currentPath, o)
183+
if sourceVal.Type().Implements(reflect.TypeOf((*NavigableNoder)(nil)).Elem()) {
184+
val, stack, err := getNavigableNoderTarget(sourceVal, currentPart, stack, currentPath, o)
185+
if err != nil {
186+
if !errors.Is(err, ErrSkipInterface) {
187+
return nil, nil, err
188+
}
189+
} else {
190+
return val, stack, nil
191+
}
186192
}
187193

188-
if sourceVal.Type().Implements(reflect.TypeOf((*IndexNavigable)(nil)).Elem()) {
189-
return getNavigableWithIndexTarget(sourceVal, currentPart, stack, currentPath, o)
194+
switch currentPart.Type {
195+
case partTypeKey:
196+
return getKeyBasedStructTarget(sourceVal, currentPart, stack, currentPath, o)
197+
case partTypeIndex:
198+
return getIndexBasedStructTarget(sourceVal, currentPart, stack, currentPath, o)
199+
default:
200+
return nil, nil, ErrInvalidPath.Wrap(fmt.Errorf("expected key or index, got %s at %s", currentPart.Type, currentPath))
190201
}
202+
}
191203

192-
if sourceVal.Type().Implements(reflect.TypeOf((*NavigableNoder)(nil)).Elem()) {
193-
return getNavigableNoderTarget(sourceVal, currentPart, stack, currentPath, o)
204+
func getKeyBasedStructTarget(sourceVal reflect.Value, currentPart navigationPart, stack []navigationPart, currentPath string, o *options) (any, []navigationPart, error) {
205+
if sourceVal.Type().Implements(reflect.TypeOf((*KeyNavigable)(nil)).Elem()) {
206+
val, stack, err := getNavigableWithKeyTarget(sourceVal, currentPart, stack, currentPath, o)
207+
if err != nil {
208+
if !errors.Is(err, ErrSkipInterface) {
209+
return nil, nil, err
210+
}
211+
} else {
212+
return val, stack, nil
213+
}
194214
}
195215

196216
if sourceVal.Kind() == reflect.Ptr && sourceVal.IsNil() {
@@ -199,6 +219,8 @@ func getStructTarget(sourceVal reflect.Value, currentPart navigationPart, stack
199219

200220
key := currentPart.unescapeValue()
201221

222+
sourceValElem := reflect.Indirect(sourceVal)
223+
202224
for i := 0; i < sourceValElem.NumField(); i++ {
203225
field := sourceValElem.Type().Field(i)
204226
if !field.IsExported() {
@@ -227,6 +249,22 @@ func getStructTarget(sourceVal reflect.Value, currentPart navigationPart, stack
227249
return nil, nil, ErrNotFound.Wrap(fmt.Errorf("key %s not found in %v at %s", key, sourceVal.Type(), currentPath))
228250
}
229251

252+
func getIndexBasedStructTarget(sourceVal reflect.Value, currentPart navigationPart, stack []navigationPart, currentPath string, o *options) (any, []navigationPart, error) {
253+
if sourceVal.Type().Implements(reflect.TypeOf((*IndexNavigable)(nil)).Elem()) {
254+
val, stack, err := getNavigableWithIndexTarget(sourceVal, currentPart, stack, currentPath, o)
255+
if err != nil {
256+
if errors.Is(err, ErrSkipInterface) {
257+
return nil, nil, fmt.Errorf("can't navigate by index on %s at %s", sourceVal.Type(), currentPath)
258+
}
259+
return nil, nil, err
260+
} else {
261+
return val, stack, nil
262+
}
263+
} else {
264+
return nil, nil, ErrNotFound.Wrap(fmt.Errorf("expected IndexNavigable, got %s at %s", sourceVal.Kind(), currentPath))
265+
}
266+
}
267+
230268
func getNavigableWithKeyTarget(sourceVal reflect.Value, currentPart navigationPart, stack []navigationPart, currentPath string, o *options) (any, []navigationPart, error) {
231269
if sourceVal.Kind() == reflect.Ptr && sourceVal.IsNil() {
232270
return nil, nil, ErrNotFound.Wrap(fmt.Errorf("source is nil at %s", currentPath))
@@ -277,7 +315,10 @@ func getNavigableNoderTarget(sourceVal reflect.Value, currentPart navigationPart
277315
return nil, nil, ErrNotFound.Wrap(fmt.Errorf("expected navigableNoder, got %s at %s", sourceVal.Kind(), currentPath))
278316
}
279317

280-
value := nn.GetNavigableNode()
318+
value, err := nn.GetNavigableNode()
319+
if err != nil {
320+
return nil, nil, err
321+
}
281322

282323
return getTarget(value, currentPart, stack, currentPath, o)
283324
}

jsonpointer/jsonpointer_test.go

Lines changed: 161 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package jsonpointer
22

33
import (
44
"errors"
5+
"fmt"
56
"testing"
67

78
"github.com/speakeasy-api/openapi/sequencedmap"
@@ -382,7 +383,166 @@ func TestGetTarget_Error(t *testing.T) {
382383
source: TestStruct{},
383384
pointer: JSONPointer("/1"),
384385
},
385-
wantErr: errors.New("invalid path -- expected key, got index at /1"),
386+
wantErr: errors.New("not found -- expected IndexNavigable, got struct at /1"),
387+
},
388+
}
389+
for _, tt := range tests {
390+
t.Run(tt.name, func(t *testing.T) {
391+
target, err := GetTarget(tt.args.source, tt.args.pointer, tt.args.opts...)
392+
assert.EqualError(t, err, tt.wantErr.Error())
393+
assert.Nil(t, target)
394+
})
395+
}
396+
}
397+
398+
type InterfaceTestStruct struct {
399+
typ string
400+
valuesByKey map[string]any
401+
valuesByIndex []any
402+
Field1 any
403+
Field2 any
404+
}
405+
406+
var (
407+
_ KeyNavigable = (*InterfaceTestStruct)(nil)
408+
_ IndexNavigable = (*InterfaceTestStruct)(nil)
409+
)
410+
411+
func (t InterfaceTestStruct) NavigateWithKey(key string) (any, error) {
412+
switch t.typ {
413+
case "map":
414+
return t.valuesByKey[key], nil
415+
case "struct":
416+
return nil, ErrSkipInterface
417+
case "slice":
418+
return nil, ErrInvalidPath
419+
default:
420+
return nil, fmt.Errorf("unknown type %s", t.typ)
421+
}
422+
}
423+
424+
func (t InterfaceTestStruct) NavigateWithIndex(index int) (any, error) {
425+
switch t.typ {
426+
case "map":
427+
return nil, ErrInvalidPath
428+
case "struct":
429+
return nil, ErrSkipInterface
430+
case "slice":
431+
return t.valuesByIndex[index], nil
432+
default:
433+
return nil, fmt.Errorf("unknown type %s", t.typ)
434+
}
435+
}
436+
437+
type NavigableNodeWrapper struct {
438+
typ string
439+
NavigableNode InterfaceTestStruct
440+
Field1 any
441+
Field2 any
442+
}
443+
444+
var _ NavigableNoder = (*NavigableNodeWrapper)(nil)
445+
446+
func (n NavigableNodeWrapper) GetNavigableNode() (any, error) {
447+
switch n.typ {
448+
case "wrapper":
449+
return n.NavigableNode, nil
450+
case "struct":
451+
return nil, ErrSkipInterface
452+
case "other":
453+
return nil, ErrInvalidPath
454+
default:
455+
return nil, fmt.Errorf("unknown type %s", n.typ)
456+
}
457+
}
458+
459+
func TestGetTarget_WithInterfaces_Success(t *testing.T) {
460+
type args struct {
461+
source any
462+
pointer JSONPointer
463+
opts []option
464+
}
465+
tests := []struct {
466+
name string
467+
args args
468+
want any
469+
}{
470+
{
471+
name: "KeyNavigable succeeds",
472+
args: args{
473+
source: InterfaceTestStruct{typ: "map", valuesByKey: map[string]any{"key1": "value1"}},
474+
pointer: JSONPointer("/key1"),
475+
},
476+
want: "value1",
477+
},
478+
{
479+
name: "IndexNavigable succeeds",
480+
args: args{
481+
source: InterfaceTestStruct{typ: "slice", valuesByIndex: []any{"value1", "value2"}},
482+
pointer: JSONPointer("/1"),
483+
},
484+
want: "value2",
485+
},
486+
{
487+
name: "Struct is navigable",
488+
args: args{
489+
source: InterfaceTestStruct{typ: "struct", Field1: "value1"},
490+
pointer: JSONPointer("/Field1"),
491+
},
492+
want: "value1",
493+
},
494+
{
495+
name: "NavigableNoder succeeds",
496+
args: args{
497+
source: NavigableNodeWrapper{typ: "wrapper", NavigableNode: InterfaceTestStruct{typ: "struct", Field1: "value1"}},
498+
pointer: JSONPointer("/Field1"),
499+
},
500+
want: "value1",
501+
},
502+
{
503+
name: "NavigableNoder struct is navigable",
504+
args: args{
505+
source: NavigableNodeWrapper{typ: "struct", Field2: "value2"},
506+
pointer: JSONPointer("/Field2"),
507+
},
508+
want: "value2",
509+
},
510+
}
511+
for _, tt := range tests {
512+
t.Run(tt.name, func(t *testing.T) {
513+
target, err := GetTarget(tt.args.source, tt.args.pointer, tt.args.opts...)
514+
require.NoError(t, err)
515+
assert.Equal(t, tt.want, target)
516+
})
517+
}
518+
}
519+
520+
func TestGetTarget_WithInterfaces_Error(t *testing.T) {
521+
type args struct {
522+
source any
523+
pointer JSONPointer
524+
opts []option
525+
}
526+
tests := []struct {
527+
name string
528+
args args
529+
wantErr error
530+
}{
531+
{
532+
name: "Error returned for invalid KeyNavigable type",
533+
args: args{
534+
source: InterfaceTestStruct{typ: "slice", valuesByIndex: []any{"value1", "value2"}},
535+
pointer: JSONPointer("/key2"),
536+
},
537+
wantErr: errors.New("not found -- invalid path"),
538+
},
539+
{
540+
name: "Error returned for invalid IndexNavigable type",
541+
args: args{
542+
source: InterfaceTestStruct{typ: "struct", Field1: "value1"},
543+
pointer: JSONPointer("/1"),
544+
},
545+
wantErr: errors.New("can't navigate by index on jsonpointer.InterfaceTestStruct at /1"),
386546
},
387547
}
388548
for _, tt := range tests {

jsonschema/oas31/core/value.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@ func (v *EitherValue[L, R]) SyncChanges(ctx context.Context, model any, valueNod
7272
}
7373
}
7474

75-
func (v *EitherValue[L, R]) GetNavigableNode() any {
75+
func (v *EitherValue[L, R]) GetNavigableNode() (any, error) {
7676
if v.Left != nil {
77-
return v.Left
77+
return v.Left, nil
7878
}
79-
return v.Right
79+
return v.Right, nil
8080
}
8181

8282
func unmarshalValue[T any](ctx context.Context, node *yaml.Node) (*T, []error) {

marshaller/node.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,6 @@ func (n Node[V]) GetMapValueNodeOrRoot(key string, rootNode *yaml.Node) *yaml.No
120120
return n.ValueNode
121121
}
122122

123-
func (n Node[V]) GetNavigableNode() any {
124-
return n.Value
123+
func (n Node[V]) GetNavigableNode() (any, error) {
124+
return n.Value, nil
125125
}

0 commit comments

Comments
 (0)