Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/loud-mails-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@redocly/openapi-core": patch
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also affects the CLI behaviour, so please mention it here as well.

"@redocly/cli": patch
---

Fixed an issue where the content of `$ref`s inside example values was erroneously resolved during bundling and linting.
2 changes: 1 addition & 1 deletion docs/@v2/configuration/reference/resolve.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ One HTTP header is supported for each URL resolved.

- doNotResolveExamples
- boolean
- When running `lint`, set this option to `true` to avoid resolving `$ref` fields in examples. Resolving `$ref`s in other parts of the API is unaffected.
- Set this option to `true` to prevent resolving `$ref` fields in singular `example` properties. This affects both `lint` and `bundle` commands. Resolving `$ref`s in other parts of the API description is unaffected.

---

Expand Down
3 changes: 2 additions & 1 deletion docs/@v2/configuration/resolve.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ properties:
doNotResolveExamples:
type: boolean
description: >-
You can stop `lint` from resolving `$refs` in examples by setting this configuration option to `true`.
Set this option to `true` to prevent resolving `$ref` fields in singular `example` properties.
This affects both `lint` and `bundle` commands.
This does not affect `$ref` resolution in other parts of the API description.
default: false
http:
Expand Down
229 changes: 229 additions & 0 deletions packages/core/src/__tests__/bundle-examples-ref-resolution.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import outdent from 'outdent';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { bundleDocument } from '../bundle/bundle-document.js';
import { parseYamlToDocument, yamlSerializer } from '../../__tests__/utils.js';
import { createConfig } from '../config/index.js';
import { BaseResolver } from '../resolve.js';
import { AsyncApi3Types, Oas3Types } from '../index.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

describe('Bundle Examples $ref Resolution', () => {
expect.addSnapshotSerializer(yamlSerializer);

describe('OpenAPI 3.0/3.1 Examples', () => {
it('should resolve $ref in Example field', async () => {
const testDocument = parseYamlToDocument(
outdent`
openapi: 3.0.0
info:
title: Test API
version: 1.0.0
paths:
/test:
get:
responses:
'200':
content:
application/json:
schema:
type: object
properties:
id:
type: integer
name:
type: string
example:
$ref: ${path.join(__dirname, 'fixtures/example-data.json')}
`,
''
);

const { bundle: result, problems } = await bundleDocument({
document: testDocument,
externalRefResolver: new BaseResolver(),
config: await createConfig({}),
types: Oas3Types,
});

expect(problems).toHaveLength(0);
expect(result.parsed).toMatchInlineSnapshot(`
openapi: 3.0.0
info:
title: Test API
version: 1.0.0
paths:
/test:
get:
responses:
'200':
content:
application/json:
schema:
type: object
properties:
id:
type: integer
name:
type: string
example:
id: 123
name: Test User
components: {}
`);
});

it('should NOT resolve $ref in Example.value field', async () => {
const testDocument = parseYamlToDocument(
outdent`
openapi: 3.1.0
info:
title: Test API
version: 1.0.0
paths:
/test:
get:
responses:
'200':
content:
application/json:
examples:
test-example:
value:
$ref: ./example-data.json
`,
''
);

const { bundle: result, problems } = await bundleDocument({
document: testDocument,
externalRefResolver: new BaseResolver(),
config: await createConfig({}),
types: Oas3Types,
});

expect(problems).toHaveLength(0);
expect(result.parsed).toMatchInlineSnapshot(`
openapi: 3.1.0
info:
title: Test API
version: 1.0.0
paths:
/test:
get:
responses:
'200':
content:
application/json:
examples:
test-example:
value:
$ref: ./example-data.json
components: {}
`);
});
});

describe('OpenAPI 3.2 Examples', () => {
it('should NOT resolve $ref in Example.dataValue field', async () => {
const testDocument = parseYamlToDocument(
outdent`
openapi: 3.2.0
info:
title: Test API
version: 1.0.0
paths:
/test:
get:
responses:
'200':
content:
application/json:
examples:
test-example:
dataValue:
$ref: ./example-data.json
`,
''
);

const { bundle: result, problems } = await bundleDocument({
document: testDocument,
externalRefResolver: new BaseResolver(),
config: await createConfig({}),
types: Oas3Types,
});

expect(problems).toHaveLength(0);
expect(result.parsed).toMatchInlineSnapshot(`
openapi: 3.2.0
info:
title: Test API
version: 1.0.0
paths:
/test:
get:
responses:
'200':
content:
application/json:
examples:
test-example:
dataValue:
$ref: ./example-data.json
components: {}
`);
});
});

describe('AsyncAPI 3.0 Examples', () => {
it('should NOT resolve $ref in Example.value field', async () => {
const testDocument = parseYamlToDocument(
outdent`
asyncapi: 3.0.0
info:
title: Test AsyncAPI
version: 1.0.0
operations:
sendUserSignedup:
action: send
messages:
- payload:
type: object
examples:
test-example:
value:
$ref: ./example-data.json
`,
''
);

const { bundle: result, problems } = await bundleDocument({
document: testDocument,
externalRefResolver: new BaseResolver(),
config: await createConfig({}),
types: AsyncApi3Types,
});

expect(problems).toHaveLength(0);
expect(result.parsed).toMatchInlineSnapshot(`
asyncapi: 3.0.0
info:
title: Test AsyncAPI
version: 1.0.0
operations:
sendUserSignedup:
action: send
messages:
- payload:
type: object
examples:
test-example:
value:
$ref: ./example-data.json
components: {}
`);
});
});
});
4 changes: 4 additions & 0 deletions packages/core/src/__tests__/fixtures/example-data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"id": 123,
"name": "Test User"
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,38 @@ describe('Referenceable scalars', () => {
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
});

it('should not report example value with $ref', async () => {
const document = parseYamlToDocument(
outdent`
openapi: 3.2.0
info:
title: Test
version: '1.0'
paths:
'/test':
get:
parameters:
- name: test
schema:
type: object
examples:
test:
value:
$ref: not $ref, example
`,
__dirname + '/foobar.yaml'
);

const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await createConfig({
rules: {
'no-unresolved-refs': 'error',
},
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
});
});
1 change: 1 addition & 0 deletions packages/core/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type ScalarSchema = {
items?: ScalarSchema;
enum?: string[];
isExample?: boolean;
resolvable?: boolean;
directResolveAs?: string;
minimum?: number;
};
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/types/oas3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ const MediaType: NodeType = {

const Example: NodeType = {
properties: {
value: { isExample: true },
value: { resolvable: false },
summary: { type: 'string' },
description: { type: 'string' },
externalValue: { type: 'string' },
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/types/oas3_2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ const Example: NodeType = {
...Oas3_1Types.Example,
properties: {
...Oas3_1Types.Example.properties,
dataValue: { isExample: true },
dataValue: { resolvable: false },
serializedValue: { type: 'string' },
},
};
Expand Down
28 changes: 28 additions & 0 deletions tests/e2e/bundle/bundle-example-value-with-ref/openapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
openapi: 3.2.0
info:
title: Test API
version: 1.0.0
paths:
/test:
get:
responses:
'200':
content:
application/json:
schema:
type: object
properties:
$ref:
type: string
example:
$ref: example.json # should NOT be resolved
examples:
Test:
summary: Example with $ref in value (should NOT resolve)
value:
$ref: example.json # should NOT be resolved

TestDataValue:
summary: Example with $ref in dataValue (should NOT resolve)
dataValue:
$ref: example.json # should NOT be resolved
6 changes: 6 additions & 0 deletions tests/e2e/bundle/bundle-example-value-with-ref/redocly.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apis:
main:
root: openapi.yaml

resolve:
doNotResolveExamples: true
31 changes: 31 additions & 0 deletions tests/e2e/bundle/bundle-example-value-with-ref/snapshot.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
openapi: 3.2.0
info:
title: Test API
version: 1.0.0
paths:
/test:
get:
responses:
'200':
content:
application/json:
schema:
type: object
properties:
$ref:
type: string
example:
$ref: example.json
examples:
Test:
summary: Example with $ref in value (should NOT resolve)
value:
$ref: example.json
TestDataValue:
summary: Example with $ref in dataValue (should NOT resolve)
dataValue:
$ref: example.json
components: {}

bundling openapi.yaml using configuration for api 'main'...
📦 Created a bundle for openapi.yaml at stdout <test>ms.
Loading