From 6a49b385538d249a21d192bce5604a093e991c63 Mon Sep 17 00:00:00 2001 From: Barrett Schonefeld Date: Fri, 26 Mar 2021 12:00:26 -0500 Subject: [PATCH] feat: validate x-sdk-operations extension Changes: - add custom Spectral rule to validate x-sdk-operations - add custom Spectral function to invoke ajv JSON Schema validator on x-sdk-operations JSON Schema Tests: - ensure warnings issued for invalid x-sdk-operations schema Docs: - document `ibm-sdk-operations` rule --- docs/spectral-rules.md | 6 + .../rulesets/.defaultsForSpectral.yaml | 14 ++- .../rulesets/ibm-oas/ibm-sdk-operations.js | 118 ++++++++++++++++++ .../custom-rules/ibm-sdk-operations.test.js | 53 ++++++++ 4 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 src/spectral/rulesets/ibm-oas/ibm-sdk-operations.js create mode 100644 test/spectral/tests/custom-rules/ibm-sdk-operations.test.js diff --git a/docs/spectral-rules.md b/docs/spectral-rules.md index 6adb1d5a3..8e371b635 100644 --- a/docs/spectral-rules.md +++ b/docs/spectral-rules.md @@ -56,6 +56,12 @@ responses: **Default Severity**: warn +## ibm-sdk-operations + +Validates the structure of the `x-sdk-operations` object. + +**Default Severity**: warn + ## response-error-response-schema `4xx` and `5xx` error responses should provide good information to help the user resolve the error. The error response validations are based on the design principles outlined in the [errors section of the IBM API Handbook](https://cloud.ibm.com/docs/api-handbook?topic=api-handbook-errors). The `response-error-response-schema` rule is more lenient than what is outlined in the handbook. Specifically, the `response-error-response-schema` rule does not require an Error Container Model and allows for a single Error Model to be provided at the top level of the error response schema or in an `error` field. diff --git a/src/spectral/rulesets/.defaultsForSpectral.yaml b/src/spectral/rulesets/.defaultsForSpectral.yaml index 700421f03..88bbf7121 100644 --- a/src/spectral/rulesets/.defaultsForSpectral.yaml +++ b/src/spectral/rulesets/.defaultsForSpectral.yaml @@ -2,6 +2,7 @@ extends: spectral:oas functionsDir: './ibm-oas' functions: - error-response-schema + - ibm-sdk-operations - response-example-provided rules: @@ -101,8 +102,8 @@ rules: content-entry-provided: description: Request bodies and non-204 responses should define a content object given: - - $.paths[*][*].responses[?(@property != '204')] - - $.paths[*][*].requestBody + - $.paths[*][*].responses[?(@property != '204')] + - $.paths[*][*].requestBody severity: warn formats: ["oas3"] resolved: true @@ -120,6 +121,15 @@ rules: then: field: schema function: truthy + # custom Spectral rule to ensure valid x-sdk-operations schema + ibm-sdk-operations: + message: "{{error}}" + given: $.paths[*][*][x-sdk-operations] + severity: warn + formats: ["oas3"] + resolved: true + then: + function: ibm-sdk-operations # custom Spectral rule to ensure response example provided response-example-provided: message: "{{error}}" diff --git a/src/spectral/rulesets/ibm-oas/ibm-sdk-operations.js b/src/spectral/rulesets/ibm-oas/ibm-sdk-operations.js new file mode 100644 index 000000000..e561263e2 --- /dev/null +++ b/src/spectral/rulesets/ibm-oas/ibm-sdk-operations.js @@ -0,0 +1,118 @@ +const AJV = require('ajv'); +const ajv = new AJV({ allErrors: true, jsonPointers: true }); +const jsonSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + title: 'IBM SDK Operations Extension', + description: 'sdk operations extension schema', + properties: { + 'x-sdk-operations': { + properties: { + 'request-examples': { + additionalProperties: { + $ref: '#/definitions/x-sdk-request-examples-array' + } + } + }, + additionalProperties: false + } + }, + definitions: { + 'x-sdk-request-examples-array': { + type: 'array', + items: { + $ref: '#/definitions/x-sdk-request-example' + } + }, + 'x-sdk-request-example': { + properties: { + name: { + type: 'string', + description: + 'The name or title of the example. In documentation it should appear as a header above the example.' + }, + contentType: { + type: 'array', + items: { + type: 'string', + description: 'The media type of the response in this example.' + } + }, + example: { + type: 'array', + description: + 'An array of code or text elements that make up the example', + items: { + $ref: '#/definitions/x-sdk-request-example-element' + } + }, + response: {}, + description: { + type: 'string' + } + }, + required: ['name', 'example'], + additionalProperties: false + }, + 'x-sdk-request-example-element': { + description: 'An element of the request example.', + properties: { + name: { + type: 'string', + description: + 'The name or title of the example element. In documentation it should appear as a header above the example element.' + }, + type: { + type: 'string', + description: + 'The element type indicates the type of content in the element. `text` elements contain a textual description or explanation, possibly using markdown for rich text elements. `code` elements contain code appropriate for the language of the request example. `code` elements will be presented in a `
` block in documentation and should contain no markup or escapes other than escapes for quote and backslash (required for JSON).',
+          enum: ['text', 'code']
+        },
+        source: {
+          type: 'array',
+          items: {
+            type: 'string'
+          },
+          description:
+            'Content of the example element as an array of strings. The content is formed by simple concatenation of the array elements.'
+        }
+      },
+      required: ['type', 'source']
+    }
+  }
+};
+
+module.exports = function(sdkOperations, _opts, paths) {
+  const errors = [];
+  const sdkSchema = {
+    'x-sdk-operations': sdkOperations
+  };
+  const rootPath = paths.target !== void 0 ? paths.target : paths.given;
+  const validate = ajv.compile(jsonSchema);
+  if (!ajv.validate(jsonSchema, sdkSchema)) {
+    return formatAJVErrors(validate.errors, rootPath);
+  }
+  return errors;
+};
+
+function formatAJVErrors(errors, rootPath) {
+  const errorList = [];
+  errors.forEach(function(err) {
+    errorList.push({
+      message: err.message,
+      path: getErrPath(err, rootPath)
+    });
+  });
+  return errorList;
+}
+
+function getErrPath(err, rootPath) {
+  const relativePath = err.dataPath.split('/');
+  if (relativePath.length > 1) {
+    const strippedPath = relativePath.splice(
+      relativePath.indexOf('x-sdk-operations') + 1,
+      relativePath.length
+    );
+    return [...rootPath, ...strippedPath];
+  }
+  return rootPath;
+}
diff --git a/test/spectral/tests/custom-rules/ibm-sdk-operations.test.js b/test/spectral/tests/custom-rules/ibm-sdk-operations.test.js
new file mode 100644
index 000000000..9b7c61bcb
--- /dev/null
+++ b/test/spectral/tests/custom-rules/ibm-sdk-operations.test.js
@@ -0,0 +1,53 @@
+const inCodeValidator = require('../../../../src/lib');
+
+describe('spectral - test that x-sdk-operations schema violations cause errors', function() {
+  let res;
+
+  beforeAll(async () => {
+    const spec = {
+      openapi: '3.0.0',
+      info: {
+        version: '1.0.0',
+        title: 'ErrorAPI'
+      },
+      servers: [{ url: 'http://api.errorapi.com/v1' }],
+      paths: {
+        path1: {
+          post: {
+            'x-sdk-operations': {
+              'request-examples': {
+                type: 13,
+                notafield: 'asdf'
+              }
+            }
+          }
+        }
+      }
+    };
+
+    res = await inCodeValidator(spec, true);
+  });
+
+  it('should warn for invalid x-sdk-operations schema', function() {
+    const expectedWarnings = res.warnings.filter(
+      warn => warn.rule === 'ibm-sdk-operations'
+    );
+    expect(expectedWarnings.length).toBe(2);
+    expect(expectedWarnings[0].path).toEqual([
+      'paths',
+      'path1',
+      'post',
+      'x-sdk-operations',
+      'request-examples',
+      'type'
+    ]);
+    expect(expectedWarnings[1].path).toEqual([
+      'paths',
+      'path1',
+      'post',
+      'x-sdk-operations',
+      'request-examples',
+      'notafield'
+    ]);
+  });
+});