Skip to content

Commit 2f635d9

Browse files
committed
fix: resolve $ref siblings during bundling
1 parent 3e802f1 commit 2f635d9

11 files changed

Lines changed: 352 additions & 7 deletions

File tree

.changeset/plain-eagles-fry.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@redocly/openapi-core": patch
3+
"@redocly/cli": patch
4+
---
5+
6+
Fixed an issue where sibling properties next to `$ref` were not resolved during bundling.

packages/core/src/__tests__/bundle.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -843,4 +843,69 @@ describe('sibling $ref resolution by spec', () => {
843843
expect(problems).toHaveLength(0);
844844
expect(res.parsed).toMatchSnapshot();
845845
});
846+
847+
it('should bundle nested external refs inside Schema $ref sibling properties', async () => {
848+
const { bundle: res, problems } = await bundle({
849+
config: await createConfig({}),
850+
ref: path.join(
851+
__dirname,
852+
'fixtures/sibling-refs/openapi-schema-ref-sibling-nested-external.yaml'
853+
),
854+
});
855+
856+
const parsed = res.parsed as any;
857+
const nestedSiblingRef =
858+
parsed.paths['/sample'].get.responses['200'].content['application/json'].schema.properties
859+
.second.$ref;
860+
861+
expect(problems).toHaveLength(0);
862+
expect(nestedSiblingRef).toBe('#/components/schemas/Second');
863+
expect(parsed.components.schemas.Second).toMatchObject({
864+
title: 'Referenced model',
865+
type: 'string',
866+
});
867+
});
868+
869+
it('should keep sibling fields defined next to a schema $ref wrapper', async () => {
870+
const {
871+
bundle: { parsed },
872+
problems,
873+
} = await bundleFromString({
874+
source: outdent`
875+
openapi: 3.1.0
876+
info:
877+
title: Sibling fields on schema $ref wrapper
878+
version: 1.0.0
879+
paths:
880+
/sample:
881+
get:
882+
responses:
883+
'200':
884+
description: OK
885+
content:
886+
application/json:
887+
schema:
888+
$ref: '#/components/schemas/IdReadOnly'
889+
components:
890+
schemas:
891+
Id:
892+
type: integer
893+
description: A generic id
894+
IdReadOnly:
895+
$ref: '#/components/schemas/Id'
896+
readOnly: true
897+
description: A generic id that is specifically read only
898+
`,
899+
config: await createConfig({}),
900+
});
901+
902+
const wrappedSchema = (parsed as any).components.schemas.IdReadOnly;
903+
904+
expect(problems).toHaveLength(0);
905+
expect(wrappedSchema).toMatchObject({
906+
$ref: '#/components/schemas/Id',
907+
readOnly: true,
908+
description: 'A generic id that is specifically read only',
909+
});
910+
});
846911
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
openapi: 3.1.0
2+
info:
3+
title: Nested external refs in schema $ref siblings
4+
version: 1.0.0
5+
paths:
6+
/sample:
7+
get:
8+
responses:
9+
'200':
10+
description: OK
11+
content:
12+
application/json:
13+
schema:
14+
$ref: '#/components/schemas/Base'
15+
properties:
16+
second:
17+
$ref: ./schema-ref-sibling-external-value.yaml#/Second
18+
components:
19+
schemas:
20+
Base:
21+
type: object
22+
properties:
23+
first:
24+
type: integer
25+
second:
26+
type: integer
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Second:
2+
title: Referenced model
3+
type: string

packages/core/src/walk.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -325,11 +325,6 @@ export function walkDocument<T extends BaseVisitor>(opts: {
325325

326326
let loc = resolvedLocation;
327327

328-
if (value === undefined) {
329-
value = node[propName];
330-
loc = location; // properties on the same level as $ref should resolve against original location, not target
331-
}
332-
333328
let propType = getOwn(type.properties, propName);
334329
if (propType === undefined) propType = type.additionalProperties;
335330
if (typeof propType === 'function') propType = propType(value, propName);
@@ -351,12 +346,16 @@ export function walkDocument<T extends BaseVisitor>(opts: {
351346
propType = { name: 'scalar', properties: {} };
352347
}
353348

354-
if (isRef(node[propName]) && propType?.name === 'scalar') {
349+
if (!isNamedType(propType)) {
350+
continue;
351+
}
352+
353+
if (resolvedNode !== node && node[propName] !== undefined) {
355354
walkNode(node[propName], propType, location.child([propName]), node, propName);
356355
continue;
357356
}
358357

359-
if (!isNamedType(propType) || (propType.name === 'scalar' && !isRef(value))) {
358+
if (propType.name === 'scalar' && !isRef(value)) {
360359
continue;
361360
}
362361

tests/e2e/bundle/bundle.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,21 @@ describe('bundle with long description', () => {
200200
await expect(cleanupOutput(result)).toMatchFileSnapshot(join(testPath, 'snapshot_2.txt'));
201201
});
202202
});
203+
204+
describe('bundle with ref siblings', () => {
205+
it('should resolve schema ref siblings', async () => {
206+
const testPath = join(__dirname, `ref-siblings`);
207+
const args = getParams(indexEntryPoint, ['bundle', 'openapi.yaml']);
208+
209+
const result = cleanupOutput(getCommandOutput(args, { testPath }));
210+
await expect(cleanupOutput(result)).toMatchFileSnapshot(join(testPath, 'snapshot.txt'));
211+
});
212+
213+
it('should resolve schema ref siblings in dereferenced output', async () => {
214+
const testPath = join(__dirname, `ref-siblings`);
215+
const args = getParams(indexEntryPoint, ['bundle', 'openapi.yaml', '--dereferenced']);
216+
217+
const result = cleanupOutput(getCommandOutput(args, { testPath }));
218+
await expect(cleanupOutput(result)).toMatchFileSnapshot(join(testPath, 'snapshot_2.txt'));
219+
});
220+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
openapi: 3.1.0
2+
info:
3+
title: E2E bundle - ref with sibling keys
4+
version: 1.0.0
5+
servers:
6+
- url: https://api.redocly.com
7+
security: []
8+
paths:
9+
/sample:
10+
get:
11+
operationId: getSample
12+
description: parameter $ref with sibling name
13+
parameters:
14+
- $ref: '#/components/parameters/id'
15+
name:
16+
$ref: reference.yaml#/title
17+
responses:
18+
'200':
19+
description: JSON schema using $ref and sibling properties
20+
content:
21+
application/json:
22+
schema:
23+
$ref: '#/components/schemas/test'
24+
properties:
25+
second:
26+
$ref: reference.yaml
27+
third:
28+
type: array
29+
items:
30+
$ref: reference.yaml
31+
additionalProperties:
32+
$ref: reference.yaml
33+
/submit:
34+
post:
35+
description: requestBody $ref with sibling description
36+
operationId: postSubmit
37+
requestBody:
38+
$ref: '#/components/requestBodies/SampleBody'
39+
description:
40+
$ref: reference.yaml#/title
41+
42+
components:
43+
parameters:
44+
id:
45+
name: id
46+
in: query
47+
required: false
48+
schema:
49+
type: string
50+
schemas:
51+
test:
52+
type: object
53+
properties:
54+
first:
55+
type: integer
56+
additionalProperties: false
57+
requestBodies:
58+
SampleBody:
59+
required: true
60+
content:
61+
application/json:
62+
schema:
63+
type: object
64+
properties:
65+
value:
66+
type: string
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
apis:
2+
main:
3+
root: ./openapi.yaml
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
title: Referenced model
2+
type: string
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
openapi: 3.1.0
2+
info:
3+
title: E2E bundle - ref with sibling keys
4+
version: 1.0.0
5+
license:
6+
name: MIT
7+
identifier: MIT
8+
servers:
9+
- url: https://api.redocly.com
10+
security: []
11+
paths:
12+
/sample:
13+
get:
14+
summary: Sample GET (parameter $ref with sibling name)
15+
operationId: getSample
16+
parameters:
17+
- $ref: '#/components/parameters/id'
18+
name: Referenced model
19+
responses:
20+
'200':
21+
description: 200 response with JSON schema using $ref and sibling properties
22+
content:
23+
application/json:
24+
schema:
25+
$ref: '#/components/schemas/test'
26+
properties:
27+
second:
28+
$ref: '#/components/schemas/reference'
29+
third:
30+
type: array
31+
items:
32+
$ref: '#/components/schemas/reference'
33+
additionalProperties:
34+
title: Referenced model
35+
type: string
36+
/submit:
37+
post:
38+
summary: Sample POST (requestBody $ref with sibling description)
39+
operationId: postSubmit
40+
requestBody:
41+
$ref: '#/components/requestBodies/SampleBody'
42+
description: Referenced model
43+
responses:
44+
'204':
45+
description: No content
46+
components:
47+
parameters:
48+
id:
49+
name: id
50+
in: query
51+
required: false
52+
schema:
53+
type: string
54+
schemas:
55+
test:
56+
type: object
57+
properties:
58+
first:
59+
type: integer
60+
additionalProperties: false
61+
reference:
62+
title: Referenced model
63+
type: string
64+
requestBodies:
65+
SampleBody:
66+
required: true
67+
content:
68+
application/json:
69+
schema:
70+
type: object
71+
properties:
72+
value:
73+
type: string
74+
75+
bundling openapi.yaml using configuration for api 'main'...
76+
📦 Created a bundle for openapi.yaml at stdout <test>ms.

0 commit comments

Comments
 (0)