Skip to content

Commit e76c7aa

Browse files
fix: recursive resolution of bundled and inlined documents and handling of number based jsonpointer parts (#35)
1 parent 33ca6a5 commit e76c7aa

26 files changed

Lines changed: 1600 additions & 255 deletions

jsonpointer/jsonpointer.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,6 @@ func getMapTarget(sourceVal reflect.Value, currentPart navigationPart, stack []n
149149
sourceValElem := reflect.Indirect(sourceVal)
150150

151151
// Allow both partTypeKey and partTypeIndex for maps (integer keys should be treated as string keys)
152-
if currentPart.Type != partTypeKey && currentPart.Type != partTypeIndex {
153-
return nil, nil, ErrInvalidPath.Wrap(fmt.Errorf("expected key or index, got %s at %s", currentPart.Type, currentPath))
154-
}
155152
if sourceValElem.IsNil() {
156153
return nil, nil, ErrNotFound.Wrap(fmt.Errorf("map is nil at %s", currentPath))
157154
}

jsonpointer/jsonpointer_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,14 @@ func TestGetTarget_Success(t *testing.T) {
291291
},
292292
want: "value",
293293
},
294+
{
295+
name: "numeric string as key in map",
296+
args: args{
297+
source: map[string]any{"400": "Bad Request", "200": "OK"},
298+
pointer: JSONPointer("/400"),
299+
},
300+
want: "Bad Request",
301+
},
294302
}
295303
for _, tt := range tests {
296304
t.Run(tt.name, func(t *testing.T) {

jsonpointer/models.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ type model interface {
1515

1616
func navigateModel(sourceVal reflect.Value, currentPart navigationPart, stack []navigationPart, currentPath string, o *options) (any, []navigationPart, error) {
1717
// Models support both key-based and index-based navigation (treat index as key)
18-
if currentPart.Type != partTypeKey && currentPart.Type != partTypeIndex {
19-
return nil, nil, ErrInvalidPath.Wrap(fmt.Errorf("models only support key or index navigation, got %s at %s", currentPart.Type, currentPath))
20-
}
2118

2219
// Ensure we have a model interface
2320
if !sourceVal.CanInterface() {

jsonpointer/yamlnode.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,6 @@ func getYamlDocumentTarget(node *yaml.Node, currentPart navigationPart, stack []
5858
}
5959

6060
func getYamlMappingTarget(node *yaml.Node, currentPart navigationPart, stack []navigationPart, currentPath string, o *options) (any, []navigationPart, error) {
61-
if currentPart.Type != partTypeKey {
62-
return nil, nil, ErrInvalidPath.Wrap(fmt.Errorf("expected key, got %s at %s", currentPart.Type, currentPath))
63-
}
64-
6561
key := currentPart.unescapeValue()
6662

6763
// YAML mapping nodes have content in pairs: [key1, value1, key2, value2, ...]

jsonpointer/yamlnode_test.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,44 @@ api:
129129
assert.Equal(t, "Get user", node.Value)
130130
},
131131
},
132+
{
133+
name: "numeric string as key in yaml mapping",
134+
args: args{
135+
yamlContent: `responses:
136+
"200": "OK"
137+
"400": "Bad Request"
138+
"500": "Internal Server Error"`,
139+
pointer: JSONPointer("/responses/400"),
140+
},
141+
validate: func(t *testing.T, result any) {
142+
t.Helper()
143+
node, ok := result.(*yaml.Node)
144+
require.True(t, ok, "result should be *yaml.Node")
145+
assert.Equal(t, yaml.ScalarNode, node.Kind)
146+
assert.Equal(t, "Bad Request", node.Value)
147+
},
148+
},
149+
{
150+
name: "numeric string as key in nested yaml mapping",
151+
args: args{
152+
yamlContent: `components:
153+
responses:
154+
"400":
155+
description: "Bad Request"
156+
content:
157+
application/json:
158+
schema:
159+
type: object`,
160+
pointer: JSONPointer("/components/responses/400/description"),
161+
},
162+
validate: func(t *testing.T, result any) {
163+
t.Helper()
164+
node, ok := result.(*yaml.Node)
165+
require.True(t, ok, "result should be *yaml.Node")
166+
assert.Equal(t, yaml.ScalarNode, node.Kind)
167+
assert.Equal(t, "Bad Request", node.Value)
168+
},
169+
},
132170
}
133171

134172
for _, tt := range tests {
@@ -181,12 +219,12 @@ func TestGetTarget_YamlNode_Error(t *testing.T) {
181219
wantErr: "not found -- index 5 out of range for yaml sequence of length 3 at /items/5",
182220
},
183221
{
184-
name: "wrong type - using index on mapping",
222+
name: "numeric key not found in mapping",
185223
args: args{
186224
yamlContent: `name: test`,
187225
pointer: JSONPointer("/0"),
188226
},
189-
wantErr: "invalid path -- expected key, got index at /0",
227+
wantErr: "not found -- key 0 not found in yaml mapping at /0",
190228
},
191229
{
192230
name: "wrong type - using key on sequence",

jsonschema/oas3/resolution.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,15 @@ func (j *JSONSchema[Referenceable]) GetAbsRef() references.Reference {
5959
// Validation errors can be skipped by setting the skipValidation flag to true. This will skip the missing field errors that occur during unmarshaling.
6060
// Resolution doesn't run the Validate function on the resolved object. So if you want to fully validate the object after resolution, you need to call the Validate function manually.
6161
func (s *JSONSchema[Referenceable]) Resolve(ctx context.Context, opts ResolveOptions) ([]error, error) {
62+
targetDocument := opts.TargetDocument
63+
if targetDocument == nil {
64+
targetDocument = opts.RootDocument
65+
}
66+
6267
return resolveJSONSchemaWithTracking(ctx, (*JSONSchemaReferenceable)(unsafe.Pointer(s)), references.ResolveOptions{ //nolint:gosec
6368
TargetLocation: opts.TargetLocation,
6469
RootDocument: opts.RootDocument,
65-
TargetDocument: opts.RootDocument,
70+
TargetDocument: targetDocument,
6671
DisableExternalRefs: opts.DisableExternalRefs,
6772
VirtualFS: opts.VirtualFS,
6873
HTTPClient: opts.HTTPClient,
@@ -121,6 +126,22 @@ func (s *JSONSchema[Referenceable]) MustGetResolvedSchema() *JSONSchema[Concrete
121126
return obj
122127
}
123128

129+
func (r *JSONSchema[Referenceable]) GetReferenceResolutionInfo() *references.ResolveResult[JSONSchemaReferenceable] {
130+
if r == nil {
131+
return nil
132+
}
133+
134+
if !r.IsReference() {
135+
return nil
136+
}
137+
138+
if r.referenceResolutionCache == nil {
139+
return nil
140+
}
141+
142+
return r.referenceResolutionCache
143+
}
144+
124145
func (s *JSONSchema[Referenceable]) resolve(ctx context.Context, opts references.ResolveOptions, referenceChain []string) ([]string, []error, error) {
125146
if !s.IsReference() {
126147
return referenceChain, nil, nil

0 commit comments

Comments
 (0)