diff --git a/docs/ibm-cloud-rules.md b/docs/ibm-cloud-rules.md
index f33ea7b4..dd7158ed 100644
--- a/docs/ibm-cloud-rules.md
+++ b/docs/ibm-cloud-rules.md
@@ -63,7 +63,9 @@ which is delivered in the `@ibm-cloud/openapi-ruleset` NPM package.
* [ibm-no-nullable-properties](#ibm-no-nullable-properties)
* [ibm-no-operation-requestbody](#ibm-no-operation-requestbody)
* [ibm-no-optional-properties-in-required-body](#ibm-no-optional-properties-in-required-body)
+ * [ibm-no-ref-in-example](#ibm-no-ref-in-example)
* [ibm-no-space-in-example-name](#ibm-no-space-in-example-name)
+ * [ibm-no-superfluous-allof](#ibm-no-superfluous-allof)
* [ibm-no-unsupported-keywords](#ibm-no-unsupported-keywords)
* [ibm-openapi-tags-used](#ibm-openapi-tags-used)
* [ibm-operation-responses](#ibm-operation-responses)
@@ -351,6 +353,12 @@ should probably be required instead of optional.
oas3 |
+ibm-no-ref-in-example |
+info |
+Makes sure that $ref is not used within an example field. |
+oas3 |
+
+
ibm-no-space-in-example-name |
warn |
The name of an entry in an examples field should not contain a space. |
@@ -3533,6 +3541,66 @@ paths:
+### ibm-no-ref-in-example
+
+
+Rule id: |
+ibm-no-ref-in-example |
+
+
+Description: |
+Within an OpenAPI document, the $ref field may appear within an examples field, but it is not valid within an example field. This rule checks to make sure that $ref is not used within an example field. |
+
+
+Severity: |
+error |
+
+
+OAS Versions: |
+oas3 |
+
+
+Non-compliant example: |
+
+
+paths:
+ /v1/things:
+ post:
+ operationId: create_thing
+ description: Create a new Thing instance.
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Thing'
+ example:
+ $ref: '#/components/examples/ThingExample'
+
+ |
+
+
+Compliant example: |
+
+
+paths:
+ /v1/things:
+ post:
+ operationId: create_thing
+ description: Create a new Thing instance.
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Thing'
+ example:
+ name: 'Thing1'
+ description: 'An example Thing object'
+
+ |
+
+
+
+
### ibm-no-space-in-example-name
diff --git a/packages/ruleset/src/functions/index.js b/packages/ruleset/src/functions/index.js
index 935d966a..9c0b1f8e 100644
--- a/packages/ruleset/src/functions/index.js
+++ b/packages/ruleset/src/functions/index.js
@@ -27,6 +27,7 @@ module.exports = {
noAmbiguousPaths: require('./no-ambiguous-paths'),
noNullableProperties: require('./no-nullable-properties'),
noOperationRequestBody: require('./no-operation-requestbody'),
+ noRefInExample: require('./no-ref-in-example'),
noSuperfluousAllOf: require('./no-superfluous-allof'),
noUnsupportedKeywords: require('./no-unsupported-keywords'),
operationIdCasingConvention: require('./operationid-casing-convention'),
diff --git a/packages/ruleset/src/functions/no-ref-in-example.js b/packages/ruleset/src/functions/no-ref-in-example.js
new file mode 100644
index 00000000..ef85a94c
--- /dev/null
+++ b/packages/ruleset/src/functions/no-ref-in-example.js
@@ -0,0 +1,54 @@
+/**
+ * Copyright 2017 - 2024 IBM Corporation.
+ * SPDX-License-Identifier: Apache2.0
+ */
+
+const { isObject } = require('@ibm-cloud/openapi-ruleset-utilities');
+const { LoggerFactory } = require('../utils');
+
+let ruleId;
+let logger;
+
+module.exports = function (exampleObj, options, context) {
+ if (!logger) {
+ ruleId = context.rule.name;
+ logger = LoggerFactory.getInstance().getLogger(ruleId);
+ }
+ return noRefInExample(exampleObj, context.path);
+};
+
+/**
+ * This function will perform a recursive check to make sure that the specified
+ * "example" value does not contain a "$ref" field.
+ * @param {*} example the example value to check
+ * @param {*} path the location of 'example' within the OpenAPI document
+ * @returns an array containing zero or more error objects
+ */
+function noRefInExample(example, path) {
+ logger.debug(`${ruleId}: checking example located at: ${path.join('.')}`);
+
+ // If it's not an object, then bail out now since a $ref property is not possible.
+ if (!isObject(example)) {
+ return [];
+ }
+
+ const errors = [];
+
+ // Check "example" for a $ref property.
+ if ('$ref' in example) {
+ logger.debug(
+ `${ruleId}: found a $ref property at location: ${path.join('.')}.$ref`
+ );
+ errors.push({
+ message: '',
+ path: [...path, '$ref'],
+ });
+ }
+
+ // Check each property of "example" recursively for an object containing a $ref property.
+ for (const p in example) {
+ errors.push(...noRefInExample(example[p], [...path, p]));
+ }
+
+ return errors;
+}
diff --git a/packages/ruleset/src/ibm-oas.js b/packages/ruleset/src/ibm-oas.js
index a4d2a537..7872af77 100644
--- a/packages/ruleset/src/ibm-oas.js
+++ b/packages/ruleset/src/ibm-oas.js
@@ -141,6 +141,7 @@ module.exports = {
'ibm-no-nullable-properties': ibmRules.noNullableProperties,
'ibm-no-operation-requestbody': ibmRules.noOperationRequestBody,
'ibm-no-optional-properties-in-required-body': ibmRules.optionalRequestBody,
+ 'ibm-no-ref-in-example': ibmRules.noRefInExample,
'ibm-no-space-in-example-name': ibmRules.examplesNameContainsSpace,
'ibm-no-superfluous-allof': ibmRules.noSuperfluousAllOf,
'ibm-no-unsupported-keywords': ibmRules.noUnsupportedKeywords,
diff --git a/packages/ruleset/src/rules/index.js b/packages/ruleset/src/rules/index.js
index aa37479d..18baa343 100644
--- a/packages/ruleset/src/rules/index.js
+++ b/packages/ruleset/src/rules/index.js
@@ -35,6 +35,7 @@ module.exports = {
noAmbiguousPaths: require('./no-ambiguous-paths'),
noNullableProperties: require('./no-nullable-properties'),
noOperationRequestBody: require('./no-operation-requestbody'),
+ noRefInExample: require('./no-ref-in-example'),
noSuperfluousAllOf: require('./no-superfluous-allof'),
noUnsupportedKeywords: require('./no-unsupported-keywords'),
operationIdCasingConvention: require('./operationid-casing-convention'),
diff --git a/packages/ruleset/src/rules/no-ref-in-example.js b/packages/ruleset/src/rules/no-ref-in-example.js
new file mode 100644
index 00000000..a4eab27d
--- /dev/null
+++ b/packages/ruleset/src/rules/no-ref-in-example.js
@@ -0,0 +1,18 @@
+/**
+ * Copyright 2017 - 2024 IBM Corporation.
+ * SPDX-License-Identifier: Apache2.0
+ */
+
+const { oas3 } = require('@stoplight/spectral-formats');
+const { noRefInExample } = require('../functions');
+module.exports = {
+ description: 'The use of $ref is not valid within an example field',
+ message: '{{description}}',
+ given: ['$..example'],
+ severity: 'error',
+ formats: [oas3],
+ resolved: false,
+ then: {
+ function: noRefInExample,
+ },
+};
diff --git a/packages/ruleset/test/no-ref-in-example.test.js b/packages/ruleset/test/no-ref-in-example.test.js
new file mode 100644
index 00000000..779a7bd3
--- /dev/null
+++ b/packages/ruleset/test/no-ref-in-example.test.js
@@ -0,0 +1,319 @@
+/**
+ * Copyright 2017 - 2024 IBM Corporation.
+ * SPDX-License-Identifier: Apache2.0
+ */
+
+const { noRefInExample } = require('../src/rules');
+const { makeCopy, rootDocument, testRule, severityCodes } = require('./utils');
+
+const rule = noRefInExample;
+const ruleId = 'ibm-no-ref-in-example';
+const expectedSeverity = severityCodes.error;
+const expectedMsg = 'The use of $ref is not valid within an example field';
+
+// To enable debug logging in the rule function, copy this statement to an it() block:
+// LoggerFactory.getInstance().addLoggerSetting(ruleId, 'debug');
+// and uncomment this import statement:
+// const LoggerFactory = require('../src/utils/logger-factory');
+
+describe(`Spectral rule: ${ruleId}`, () => {
+ describe('Should not yield errors', () => {
+ it('Clean spec', async () => {
+ const results = await testRule(ruleId, rule, rootDocument);
+ expect(results).toHaveLength(0);
+ });
+
+ it('requestBody example without $ref', async () => {
+ const testDocument = makeCopy(rootDocument);
+
+ testDocument.paths['/v1/drinks'].post.requestBody.content[
+ 'application/json'
+ ].example = {
+ name: 'coke',
+ type: 'soda',
+ };
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(0);
+ });
+
+ it('response example without $ref', async () => {
+ const testDocument = makeCopy(rootDocument);
+
+ testDocument.paths['/v1/drinks'].post.responses['201'].content[
+ 'application/json'
+ ].example = {
+ name: 'coke',
+ type: 'soda',
+ };
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(0);
+ });
+
+ it('schema example without $ref', async () => {
+ const testDocument = makeCopy(rootDocument);
+
+ testDocument.components.schemas.Drink.example = {
+ name: 'coke',
+ type: 'soda',
+ };
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(0);
+ });
+
+ it('parameter example without $ref (schema)', async () => {
+ const testDocument = makeCopy(rootDocument);
+
+ testDocument.paths['/v1/drinks'].get.parameters[0].example = 'start1';
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(0);
+ });
+ it('parameter example without $ref (content)', async () => {
+ const testDocument = makeCopy(rootDocument);
+
+ testDocument.paths['/v1/drinks'].get.parameters[0] = {
+ in: 'query',
+ name: 'start',
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'string',
+ },
+ example: 'start1',
+ },
+ },
+ };
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(0);
+ });
+ });
+
+ describe('Should yield errors', () => {
+ it('requestBody example with $ref', async () => {
+ const testDocument = makeCopy(rootDocument);
+
+ testDocument.paths['/v1/drinks'].post.requestBody.content[
+ 'application/json'
+ ].example = {
+ $ref: '#/components/schemas/Error',
+ };
+
+ const expectedPaths = [
+ 'paths./v1/drinks.post.requestBody.content.application/json.example.$ref',
+ ];
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('response example with $ref', async () => {
+ const testDocument = makeCopy(rootDocument);
+
+ testDocument.paths['/v1/drinks'].post.responses['201'].content[
+ 'application/json'
+ ].example = {
+ $ref: '#/components/schemas/Error',
+ };
+
+ const expectedPaths = [
+ 'paths./v1/drinks.post.responses.201.content.application/json.example.$ref',
+ ];
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('schema example with $ref', async () => {
+ const testDocument = makeCopy(rootDocument);
+
+ testDocument.components.schemas.Drink.example = {
+ $ref: '#/components/schemas/Error',
+ };
+
+ const expectedPaths = ['components.schemas.Drink.example.$ref'];
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('parameter example with $ref (schema)', async () => {
+ const testDocument = makeCopy(rootDocument);
+
+ testDocument.paths['/v1/drinks'].get.parameters[0].example = {
+ $ref: '#/components/schemas/Error',
+ };
+
+ const expectedPaths = ['paths./v1/drinks.get.parameters.0.example.$ref'];
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('parameter example with $ref (content)', async () => {
+ const testDocument = makeCopy(rootDocument);
+
+ testDocument.paths['/v1/drinks'].get.parameters[0] = {
+ in: 'query',
+ name: 'start',
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'string',
+ },
+ example: {
+ $ref: '#/components/schemas/Error',
+ },
+ },
+ },
+ };
+
+ const expectedPaths = [
+ 'paths./v1/drinks.get.parameters.0.content.application/json.example.$ref',
+ ];
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('requestBody example with nested $ref', async () => {
+ const testDocument = makeCopy(rootDocument);
+
+ testDocument.paths['/v1/drinks'].post.requestBody.content[
+ 'application/json'
+ ].example = {
+ name: 'Root Beer',
+ type: 'soda',
+ nested_stuff: {
+ $ref: '#/components/schemas/Error',
+ },
+ };
+
+ const expectedPaths = [
+ 'paths./v1/drinks.post.requestBody.content.application/json.example.nested_stuff.$ref',
+ ];
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('response example with nested $ref', async () => {
+ const testDocument = makeCopy(rootDocument);
+
+ testDocument.paths['/v1/drinks'].post.responses['201'].content[
+ 'application/json'
+ ].example = {
+ name: 'Root Beer',
+ type: 'soda',
+ stuff: {
+ nested_stuff: {
+ more_nested_stuff: {
+ $ref: '#/components/schemas/Error',
+ },
+ },
+ foo: 'bar',
+ },
+ };
+
+ const expectedPaths = [
+ 'paths./v1/drinks.post.responses.201.content.application/json.example.stuff.nested_stuff.more_nested_stuff.$ref',
+ ];
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('parameter example with nested $ref (content)', async () => {
+ const testDocument = makeCopy(rootDocument);
+
+ testDocument.paths['/v1/drinks'].get.parameters[0] = {
+ in: 'query',
+ name: 'start',
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ },
+ example: {
+ name: 'Root Beer',
+ type: 'soda',
+ $ref: '#/components/schemas/Error',
+ stuff: {
+ nested_stuff: {
+ more_nested_stuff: {
+ $ref: '#/components/schemas/Error',
+ },
+ },
+ foo: {
+ bar: 'baz',
+ $ref: {
+ $ref: {
+ $ref: '#/components/schemas/Error',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ };
+
+ const expectedPaths = [
+ 'paths./v1/drinks.get.parameters.0.content.application/json.example.$ref',
+ 'paths./v1/drinks.get.parameters.0.content.application/json.example.stuff.nested_stuff.more_nested_stuff.$ref',
+ 'paths./v1/drinks.get.parameters.0.content.application/json.example.stuff.foo.$ref',
+ 'paths./v1/drinks.get.parameters.0.content.application/json.example.stuff.foo.$ref.$ref',
+ 'paths./v1/drinks.get.parameters.0.content.application/json.example.stuff.foo.$ref.$ref.$ref',
+ ];
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(5);
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+ });
+});