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]);
+ }
+ });
+ });
+});