From 24198bcb32eafdeb119ee2432d8488d7a5af14aa Mon Sep 17 00:00:00 2001 From: Vazha Omanashvili Date: Fri, 7 Jun 2024 16:47:42 +0300 Subject: [PATCH] feat(rulesets): add new rule that requires sibling items field for type array (#2632) * feat(test-harness): make test-harness locale agnostic * chore(deps): use node 18.18.2 * chore(test-harness): updated test-harness readme * feat(rulesets): add new rule that requires sibling items field for type array * chore(repo): update documentation --------- Co-authored-by: Vazha Omanashvili --- .nvmrc | 2 +- docs/reference/openapi-rules.md | 31 +++++++ .../src/oas/__tests__/array-items.test.ts | 81 +++++++++++++++++++ packages/rulesets/src/oas/index.ts | 12 +++ test-harness/README.md | 15 ++-- ...quire-type-array-items-field.oas3.scenario | 51 ++++++++++++ test-harness/src/suite.ts | 10 ++- 7 files changed, 194 insertions(+), 8 deletions(-) create mode 100644 packages/rulesets/src/oas/__tests__/array-items.test.ts create mode 100644 test-harness/scenarios/require-type-array-items-field.oas3.scenario diff --git a/.nvmrc b/.nvmrc index b009dfb9d..a58d2d2c2 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -lts/* +18.18.2 \ No newline at end of file diff --git a/docs/reference/openapi-rules.md b/docs/reference/openapi-rules.md index e38848806..7b2e51187 100644 --- a/docs/reference/openapi-rules.md +++ b/docs/reference/openapi-rules.md @@ -448,6 +448,37 @@ TheBadModel: - 8 ``` +### array-items + +Schemas with `type: array`, require a sibling `items` field. + +**Recommended:** Yes + +**Good Example** + +```yaml +TheGoodModel: + type: object + properties: + favoriteColorSets: + type: array + items: + type: array + items: {} +``` + +**Bad Example** + +```yaml +TheBadModel: + type: object + properties: + favoriteColorSets: + type: array + items: + type: array +``` + ## OpenAPI v2.0-only These rules will only apply to OpenAPI v2.0 documents. diff --git a/packages/rulesets/src/oas/__tests__/array-items.test.ts b/packages/rulesets/src/oas/__tests__/array-items.test.ts new file mode 100644 index 000000000..144ca5d6e --- /dev/null +++ b/packages/rulesets/src/oas/__tests__/array-items.test.ts @@ -0,0 +1,81 @@ +import { DiagnosticSeverity } from '@stoplight/types'; + +import testRule from '../../__tests__/__helpers__/tester'; + +testRule('array-items', [ + { + name: 'valid case', + document: { + swagger: '2.0', + securityDefinitions: { + apikey: {}, + }, + paths: { + '/path': { + get: { + security: [ + { + apikey: [], + }, + ], + }, + }, + }, + }, + errors: [], + }, + + { + name: 'array items sibling is present', + document: { + $ref: '#/', + responses: { + 200: { + type: 'array', + items: {}, + }, + 201: { + type: 'array', + items: { + type: 'array', + items: {}, + }, + }, + }, + openapi: '3.0.0', + }, + errors: [], + }, + { + name: 'array items sibling is missing', + document: { + $ref: '#/', + responses: { + 200: { + type: 'array', + }, + 201: { + type: 'array', + items: { + type: 'array', + }, + }, + }, + openapi: '3.0.0', + }, + errors: [ + { + code: 'array-items', + message: 'Schemas with "type: array", require a sibling "items" field', + path: ['responses', '200'], + severity: DiagnosticSeverity.Error, + }, + { + code: 'array-items', + message: 'Schemas with "type: array", require a sibling "items" field', + path: ['responses', '201', 'items'], + severity: DiagnosticSeverity.Error, + }, + ], + }, +]); diff --git a/packages/rulesets/src/oas/index.ts b/packages/rulesets/src/oas/index.ts index 253673754..d59122c2f 100644 --- a/packages/rulesets/src/oas/index.ts +++ b/packages/rulesets/src/oas/index.ts @@ -361,6 +361,18 @@ const ruleset = { function: refSiblings, }, }, + 'array-items': { + formats: [oas3_0], + message: 'Schemas with "type: array", require a sibling "items" field', + severity: 0, + recommended: true, + resolved: false, + given: "$..[?(@.type === 'array')]", + then: { + function: truthy, + field: 'items', + }, + }, 'typed-enum': { description: 'Enum values must respect the specified type.', message: '{{error}}', diff --git a/test-harness/README.md b/test-harness/README.md index 87bfcabb2..c27e75ebe 100644 --- a/test-harness/README.md +++ b/test-harness/README.md @@ -3,7 +3,7 @@ ## Prerequisites * Install the project dependencies with `yarn` -* Generate the binary for your platform with `yarn build.binary`. This will *also* compile the project from TS -> JS +* Generate the binary for your platform with `yarn workspace @stoplight/spectral-cli build.binary`. This will *also* compile the project from TS -> JS ## Running the suite @@ -11,11 +11,16 @@ Run `yarn test.harness` from your terminal ### Running a selected tests -You can run one or selected tests using `TESTS` env variable. -If you want multiple test files to be run separate them with commas. -Use paths relative to the `./scenarios` directory. +Test Harness uses Jest under the hood. You can use all CLI options that Jest supports: https://jestjs.io/docs/cli -E.g. run `TESTS=parameters-ac1.oas2.scenario,validate-body-params/form-byte-format-fail.oas2.scenario yarn test.harness` +You can run one or multiple tests by passing a path to `test-harness` command. +All scenarios are converted to `.js` files under the `./tests` directory. +Hence you must use paths relative to the `./tests` directory, like in the following example: + +```bash +# path to scenario file: `test-harness/scenarios/require-module.scenario` +yarn test.harness test-harness/tests/require-module +``` ### Matching test files diff --git a/test-harness/scenarios/require-type-array-items-field.oas3.scenario b/test-harness/scenarios/require-type-array-items-field.oas3.scenario new file mode 100644 index 000000000..a99facf5c --- /dev/null +++ b/test-harness/scenarios/require-type-array-items-field.oas3.scenario @@ -0,0 +1,51 @@ +====test==== +Schemas with "type: array", require a sibling "items" field +====document==== +openapi: 3.0.3 +info: + title: test + description: Test specification file + version: '1.0' + contact: + name: John Doe + url: 'https://example.com' + email: john_doe@example.com + license: + name: Apache 2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0' +servers: + - url: 'http://localhost:3000' +tags: + - name: list-endpoint + description: Endpoint for listing objects +paths: + /users: + get: + summary: List Users + operationId: get-users + description: List all Users + tags: + - list-endpoint + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + favoriteColorSets: + type: array + items: + type: array + +====asset:ruleset==== +const { oas } = require('@stoplight/spectral-rulesets'); +module.exports = oas; +====command==== +{bin} lint {document} --ruleset "{asset:ruleset}" +====stdout==== +{document} + 36:27 error array-items Schemas with "type: array", require a sibling "items" field paths./users.get.responses[200].content.application/json.schema.properties.favoriteColorSets.items + +✖ 1 problem (1 error, 0 warnings, 0 infos, 0 hints) diff --git a/test-harness/src/suite.ts b/test-harness/src/suite.ts index d6377bf8d..c8e738114 100644 --- a/test-harness/src/suite.ts +++ b/test-harness/src/suite.ts @@ -19,8 +19,14 @@ if (scenario.command === null) { // executing Date() before or after spawnNode were constantly leading to occasional mismatches, // as the success of that approach was highly bound to the time spent on the actual spawnNode call // this is a tad smarter, because instead of naively hoping the date will match, we try to extract the date from the actual output - // this regular expression matches "00:43:59" in "Thu Jul 08 2021 00:43:59 GMT+0200 (Central European Summer Time)" - const date = RegExp(escapeRegExp(String(Date())).replace(/(\d\d:){2}\d\d/, '(\\d\\d:){2}\\d\\d')); + // this regular expression matches "00:43:59" in "Thu Jul 08 2021 00:43:59 GMT+0200 (Central European Summer Time)". + // this regular expression is locale agnostic: it will match "Fri May 31 2024 18:26:32 GMT+0300 (за східноєвропейським літнім часом)" + const date = RegExp( + escapeRegExp(String(Date())) + .replace(/(\d\d:){2}\d\d/, '(\\d\\d:){2}\\d\\d') + .replace(/\s\\\(.+\\\)/, '\\s+\\([\\w\\s]+\\)'), + ); + Reflect.defineProperty(env, 'date', { configurable: true, enumerable: true,