From 821c0d886f992c9dcd52f9bf103c3a7c118f77c7 Mon Sep 17 00:00:00 2001 From: Phil Adams Date: Mon, 25 Sep 2023 10:43:55 -0500 Subject: [PATCH] feat(ibm-no-unsupported-keywords): add new validation rule This commit introduces the new 'ibm-no-unsupported-keywords' validation rule which checks to make sure that the (currently) unsupported keywords 'jsonSchemaDialect' and 'webhooks' are not present in an OpenAPI 3.1 document. Signed-off-by: Phil Adams --- docs/ibm-cloud-rules.md | 77 ++++++++++++++++++ packages/ruleset/src/functions/index.js | 1 + .../src/functions/no-unsupported-keywords.js | 53 +++++++++++++ packages/ruleset/src/ibm-oas.js | 1 + packages/ruleset/src/rules/index.js | 1 + .../src/rules/no-unsupported-keywords.js | 20 +++++ .../test/no-unsupported-keywords.test.js | 79 +++++++++++++++++++ 7 files changed, 232 insertions(+) create mode 100644 packages/ruleset/src/functions/no-unsupported-keywords.js create mode 100644 packages/ruleset/src/rules/no-unsupported-keywords.js create mode 100644 packages/ruleset/test/no-unsupported-keywords.test.js diff --git a/docs/ibm-cloud-rules.md b/docs/ibm-cloud-rules.md index 922cd020b..8708f7e55 100644 --- a/docs/ibm-cloud-rules.md +++ b/docs/ibm-cloud-rules.md @@ -63,6 +63,7 @@ which is delivered in the `@ibm-cloud/openapi-ruleset` NPM package. * [ibm-no-operation-requestbody](#ibm-no-operation-requestbody) * [ibm-no-optional-properties-in-required-body](#ibm-no-optional-properties-in-required-body) * [ibm-no-space-in-example-name](#ibm-no-space-in-example-name) + * [ibm-no-unsupported-keywords](#ibm-no-unsupported-keywords) * [ibm-openapi-tags-used](#ibm-openapi-tags-used) * [ibm-operation-summary](#ibm-operation-summary) * [ibm-operationid-casing-convention](#ibm-operationid-casing-convention) @@ -343,6 +344,12 @@ should probably be required instead of optional. oas3 +ibm-no-unsupported-keywords +error +Checks for the use of unsupported keywords within an OpenAPI 3.1.x document. +oas3_1 + + ibm-openapi-tags-used warn Verifies that each defined tag is referenced by at least one operation. @@ -3432,6 +3439,76 @@ paths: +### ibm-no-unsupported-keywords + + + + + + + + + + + + + + + + + + + + + + + + + +
Rule id:ibm-no-unsupported-keywords
Description:This rule checks for the presence of specific keywords within an OpenAPI 3.1.x document that are not yet supported +by IBM's SDK-related tooling - specifically the jsonSchemaDialect and +webhooks keywords. An error is logged if either of these keywords is found in the document. +
Severity:error
OAS Versions:oas3_1
Non-compliant example: +
+openapi: 3.1.0
+info:
+  title: Thing Service
+  description: A service that manages Things
+  version: 1.0.0
+jsonSchemaDialect: 'https://spec.openapis.org/oas/3.1/dialect/base'    <<< not supported
+webhooks:                                                              <<< not supported
+  newThingTypeAvailable:
+    post:
+      description: |-
+        A callback-like operation to be implemented by the client so that it
+        can be informed of a new type of Thing supported by the server.
+      requestBody:
+        description: 'A new type of Thing can now be created on the server.'
+          content:
+            application/json:
+              schema:
+                type: object,
+                properties:
+                  thing_type:
+                    description: 'The new type value that can be used to create a Thing instance.'
+                    type: string
+      responses:
+        '200':
+          description: |-
+            Return a 200 status code to the server to indicate that the new Thing type
+            was received successfully by the client.
+
+
Compliant example: +
+openapi: 3.1.0
+info:
+  title: Thing Service
+  description: A service that manages Things
+  version: 1.0.0
+
+
+ + ### ibm-openapi-tags-used diff --git a/packages/ruleset/src/functions/index.js b/packages/ruleset/src/functions/index.js index d754a3c50..c2d41289c 100644 --- a/packages/ruleset/src/functions/index.js +++ b/packages/ruleset/src/functions/index.js @@ -25,6 +25,7 @@ module.exports = { noAmbiguousPaths: require('./no-ambiguous-paths'), noNullableProperties: require('./no-nullable-properties'), noOperationRequestBody: require('./no-operation-requestbody'), + noUnsupportedKeywords: require('./no-unsupported-keywords'), operationIdCasingConvention: require('./operationid-casing-convention'), operationIdNamingConvention: require('./operationid-naming-convention'), operationSummaryExists: require('./operation-summary-exists'), diff --git a/packages/ruleset/src/functions/no-unsupported-keywords.js b/packages/ruleset/src/functions/no-unsupported-keywords.js new file mode 100644 index 000000000..f357be307 --- /dev/null +++ b/packages/ruleset/src/functions/no-unsupported-keywords.js @@ -0,0 +1,53 @@ +/** + * Copyright 2017 - 2023 IBM Corporation. + * SPDX-License-Identifier: Apache2.0 + */ + +const { LoggerFactory } = require('../utils'); + +let ruleId; +let logger; + +const ErrorMsg = + 'An unsupported OpenAPI 3.1 keyword was found in the OpenAPI document:'; + +module.exports = function (apidef, _opts, context) { + if (!logger) { + ruleId = context.rule.name; + logger = LoggerFactory.getInstance().getLogger(ruleId); + } + return noUnsupportedKeywords(apidef); +}; + +/** + * If 'unevaluatedProperties' is specified within "schema" then it must be set to false. + * + * @param {*} apidef the API definition object + * @returns an array of zero or more errors + */ +function noUnsupportedKeywords(apidef) { + logger.debug(`${ruleId}: checking for unsupported OpenAPI 3.1 keywords`); + + const errors = []; + + if ('jsonSchemaDialect' in apidef) { + logger.debug(`${ruleId}: found 'jsonSchemaDialect'`); + errors.push({ + message: `${ErrorMsg} jsonSchemaDialect`, + path: ['jsonSchemaDialect'], + }); + } + + if ('webhooks' in apidef) { + logger.debug(`${ruleId}: found 'webhooks`); + errors.push({ + message: `${ErrorMsg} webhooks`, + path: ['webhooks'], + }); + } + + if (!errors.length) { + logger.debug(`${ruleId}: PASSED!`); + } + return errors; +} diff --git a/packages/ruleset/src/ibm-oas.js b/packages/ruleset/src/ibm-oas.js index 2acf365df..449e72502 100644 --- a/packages/ruleset/src/ibm-oas.js +++ b/packages/ruleset/src/ibm-oas.js @@ -132,6 +132,7 @@ module.exports = { 'ibm-no-operation-requestbody': ibmRules.noOperationRequestBody, 'ibm-no-optional-properties-in-required-body': ibmRules.optionalRequestBody, 'ibm-no-space-in-example-name': ibmRules.examplesNameContainsSpace, + 'ibm-no-unsupported-keywords': ibmRules.noUnsupportedKeywords, 'ibm-openapi-tags-used': ibmRules.unusedTags, 'ibm-operation-summary': ibmRules.operationSummaryExists, 'ibm-operationid-casing-convention': ibmRules.operationIdCasingConvention, diff --git a/packages/ruleset/src/rules/index.js b/packages/ruleset/src/rules/index.js index 6f82f7334..955bcf389 100644 --- a/packages/ruleset/src/rules/index.js +++ b/packages/ruleset/src/rules/index.js @@ -37,6 +37,7 @@ module.exports = { noAmbiguousPaths: require('./no-ambiguous-paths'), noNullableProperties: require('./no-nullable-properties'), noOperationRequestBody: require('./no-operation-requestbody'), + noUnsupportedKeywords: require('./no-unsupported-keywords'), operationSummaryExists: require('./operation-summary-exists'), optionalRequestBody: require('./optional-request-body'), paginationStyle: require('./pagination-style'), diff --git a/packages/ruleset/src/rules/no-unsupported-keywords.js b/packages/ruleset/src/rules/no-unsupported-keywords.js new file mode 100644 index 000000000..6ecb0e78b --- /dev/null +++ b/packages/ruleset/src/rules/no-unsupported-keywords.js @@ -0,0 +1,20 @@ +/** + * Copyright 2017 - 2023 IBM Corporation. + * SPDX-License-Identifier: Apache2.0 + */ + +const { oas3_1 } = require('@stoplight/spectral-formats'); +const { noUnsupportedKeywords } = require('../functions'); + +module.exports = { + description: + 'Verifies that unsupported OpenAPI 3.1 keywords are not used in the API document.', + message: '{{error}}', + given: ['$'], + severity: 'error', + formats: [oas3_1], + resolved: false, + then: { + function: noUnsupportedKeywords, + }, +}; diff --git a/packages/ruleset/test/no-unsupported-keywords.test.js b/packages/ruleset/test/no-unsupported-keywords.test.js new file mode 100644 index 000000000..c8978f411 --- /dev/null +++ b/packages/ruleset/test/no-unsupported-keywords.test.js @@ -0,0 +1,79 @@ +/** + * Copyright 2017 - 2023 IBM Corporation. + * SPDX-License-Identifier: Apache2.0 + */ + +const { noUnsupportedKeywords } = require('../src/rules'); +const { makeCopy, rootDocument, testRule, severityCodes } = require('./utils'); + +const rule = noUnsupportedKeywords; +const ruleId = 'ibm-no-unsupported-keywords'; +const expectedSeverity = severityCodes.error; +const expectedMsgPrefix = + 'An unsupported OpenAPI 3.1 keyword was found in the OpenAPI document:'; + +describe(`Spectral rule: ${ruleId}`, () => { + beforeAll(() => { + rootDocument.openapi = '3.1.0'; + }); + + describe('Should not yield errors', () => { + it('Clean spec - no unsupported keywords present', async () => { + const results = await testRule(ruleId, rule, rootDocument); + expect(results).toHaveLength(0); + }); + }); + + describe('Should yield errors', () => { + it('jsonSchemaDialect present', async () => { + const testDocument = makeCopy(rootDocument); + + testDocument.jsonSchemaDialect = + 'https://spec.openapis.org/oas/3.1/dialect/base'; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = ['jsonSchemaDialect']; + for (let i = 0; i < results.length; i++) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe( + `${expectedMsgPrefix} jsonSchemaDialect` + ); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + it('webhooks present', async () => { + const testDocument = makeCopy(rootDocument); + + testDocument.webhooks = { + newDrinkAvailable: { + post: { + requestBody: { + description: 'A new brand of beer is available for consumption.', + content: { + 'application/beer': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = ['webhooks']; + for (let i = 0; i < results.length; i++) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(`${expectedMsgPrefix} webhooks`); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + }); +});