From 4ececf0770f821592ecf3490fe98bc9eb0619953 Mon Sep 17 00:00:00 2001 From: stoplight-bot Date: Thu, 24 Mar 2022 19:00:20 +0000 Subject: [PATCH 01/19] chore(release): 1.7.0 [skip ci] # [@stoplight/spectral-rulesets-v1.7.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-rulesets-v1.6.0...@stoplight/spectral-rulesets-v1.7.0) (2022-03-24) ### Features * **rulesets:** add unused components server rule ([#2097](https://github.com/stoplightio/spectral/issues/2097)) ([71b312e](https://github.com/stoplightio/spectral/commit/71b312e34c85d8b4255832757b4b5afa8c5062a5)) --- packages/rulesets/CHANGELOG.md | 7 +++++++ packages/rulesets/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/rulesets/CHANGELOG.md b/packages/rulesets/CHANGELOG.md index c03c3798e..3e8762c8d 100644 --- a/packages/rulesets/CHANGELOG.md +++ b/packages/rulesets/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@stoplight/spectral-rulesets-v1.7.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-rulesets-v1.6.0...@stoplight/spectral-rulesets-v1.7.0) (2022-03-24) + + +### Features + +* **rulesets:** add unused components server rule ([#2097](https://github.com/stoplightio/spectral/issues/2097)) ([71b312e](https://github.com/stoplightio/spectral/commit/71b312e34c85d8b4255832757b4b5afa8c5062a5)) + # [@stoplight/spectral-rulesets-v1.6.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-rulesets-v1.5.2...@stoplight/spectral-rulesets-v1.6.0) (2022-03-03) diff --git a/packages/rulesets/package.json b/packages/rulesets/package.json index 7275e4b2e..140829a88 100644 --- a/packages/rulesets/package.json +++ b/packages/rulesets/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/spectral-rulesets", - "version": "1.6.0", + "version": "1.7.0", "homepage": "https://github.com/stoplightio/spectral", "bugs": "https://github.com/stoplightio/spectral/issues", "author": "Stoplight ", From aeb7d5b842741d85229fb2c8575ae2c58c8dfbb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Thu, 14 Apr 2022 14:47:54 +0200 Subject: [PATCH 02/19] feat(core): support JSON ruleset validation (#2062) --- package.json | 4 +- packages/core/package.json | 15 +- .../__fixtures__/foo-extends-bar-ruleset.json | 12 -- .../__tests__/__fixtures__/foo-ruleset.json | 14 -- .../__fixtures__/invalid-ruleset.json | 22 --- .../__fixtures__/ruleset-with-merge-keys.yaml | 20 --- .../ruleset-with-missing-functions.json | 4 - .../__fixtures__/valid-flat-ruleset-2.json | 10 -- .../__fixtures__/valid-flat-ruleset.json | 10 -- .../src/ruleset/__tests__/ruleset.test.ts | 2 +- packages/core/src/ruleset/index.ts | 2 +- packages/core/src/ruleset/mergers/rules.ts | 2 +- .../core/src/ruleset/meta/js-extensions.json | 64 ++++++++ .../src/ruleset/meta/json-extensions.json | 79 ++++++++++ .../src/{ => ruleset}/meta/rule.schema.json | 4 +- .../{ => ruleset}/meta/ruleset.schema.json | 43 ++--- .../core/src/{ => ruleset}/meta/shared.json | 7 +- packages/core/src/ruleset/validation.ts | 129 --------------- .../__tests__/__fixtures__/invalid-ruleset.ts | 24 +++ .../__fixtures__/valid-flat-ruleset.ts | 12 ++ .../__tests__/validation.test.ts | 149 +++++++++++++++--- packages/core/src/ruleset/validation/ajv.ts | 66 ++++++++ .../core/src/ruleset/validation/assertions.ts | 33 ++++ .../core/src/ruleset/validation/errors.ts | 65 ++++++++ packages/core/src/ruleset/validation/index.ts | 2 + 25 files changed, 501 insertions(+), 293 deletions(-) delete mode 100644 packages/core/src/ruleset/__tests__/__fixtures__/foo-extends-bar-ruleset.json delete mode 100644 packages/core/src/ruleset/__tests__/__fixtures__/foo-ruleset.json delete mode 100644 packages/core/src/ruleset/__tests__/__fixtures__/invalid-ruleset.json delete mode 100644 packages/core/src/ruleset/__tests__/__fixtures__/ruleset-with-merge-keys.yaml delete mode 100644 packages/core/src/ruleset/__tests__/__fixtures__/ruleset-with-missing-functions.json delete mode 100644 packages/core/src/ruleset/__tests__/__fixtures__/valid-flat-ruleset-2.json delete mode 100644 packages/core/src/ruleset/__tests__/__fixtures__/valid-flat-ruleset.json create mode 100644 packages/core/src/ruleset/meta/js-extensions.json create mode 100644 packages/core/src/ruleset/meta/json-extensions.json rename packages/core/src/{ => ruleset}/meta/rule.schema.json (94%) rename packages/core/src/{ => ruleset}/meta/ruleset.schema.json (85%) rename packages/core/src/{ => ruleset}/meta/shared.json (89%) delete mode 100644 packages/core/src/ruleset/validation.ts create mode 100644 packages/core/src/ruleset/validation/__tests__/__fixtures__/invalid-ruleset.ts create mode 100644 packages/core/src/ruleset/validation/__tests__/__fixtures__/valid-flat-ruleset.ts rename packages/core/src/ruleset/{ => validation}/__tests__/validation.test.ts (85%) create mode 100644 packages/core/src/ruleset/validation/ajv.ts create mode 100644 packages/core/src/ruleset/validation/assertions.ts create mode 100644 packages/core/src/ruleset/validation/errors.ts create mode 100644 packages/core/src/ruleset/validation/index.ts diff --git a/package.json b/package.json index 601bd616e..4f92b8ada 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "lint": "yarn lint.prettier && yarn lint.eslint", "lint.fix": "yarn lint.prettier --write && yarn lint.eslint --fix", "lint.eslint": "eslint --cache --cache-location .cache/.eslintcache --ext=.js,.mjs,.ts packages test-harness", - "lint.prettier": "prettier --ignore-path .eslintignore --ignore-unknown --check packages/core/src/meta/*.json packages/rulesets/src/{asyncapi,oas}/schemas/*.json docs/**/*.md README.md", + "lint.prettier": "prettier --ignore-path .eslintignore --ignore-unknown --check packages/core/src/ruleset/meta/*.json packages/rulesets/src/{asyncapi,oas}/schemas/*.json docs/**/*.md README.md", "pretest": "yarn workspace @stoplight/spectral-ruleset-migrator pretest", "test": "yarn pretest && yarn test.karma && yarn test.jest", "test.harness": "jest -c ./test-harness/jest.config.js", @@ -116,7 +116,7 @@ "README.md": [ "prettier --write" ], - "packages/core/src/meta/*.json": [ + "packages/core/src/ruleset/meta/*.json": [ "prettier --ignore-path .eslintignore --write" ], "packages/rulesets/src/{asyncapi,oas}/schemas/*.json": [ diff --git a/packages/core/package.json b/packages/core/package.json index e4aa4e110..da36979a3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,8 +1,6 @@ { "name": "@stoplight/spectral-core", "version": "1.11.1", - "main": "dist/index.js", - "types": "dist/index.d.ts", "sideEffects": false, "homepage": "https://github.com/stoplightio/spectral", "bugs": "https://github.com/stoplightio/spectral/issues", @@ -15,6 +13,19 @@ "files": [ "dist" ], + "type": "commonjs", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "require": "./dist/index.js" + }, + "./ruleset/validation": { + "types": "./dist/ruleset/validation/index.d.ts", + "require": "./dist/ruleset/validation/index.js" + } + }, "engines": { "node": "^12.20 || >= 14.13" }, diff --git a/packages/core/src/ruleset/__tests__/__fixtures__/foo-extends-bar-ruleset.json b/packages/core/src/ruleset/__tests__/__fixtures__/foo-extends-bar-ruleset.json deleted file mode 100644 index 0f25ca5a2..000000000 --- a/packages/core/src/ruleset/__tests__/__fixtures__/foo-extends-bar-ruleset.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": ["./bar-extends-foo-ruleset.json"], - "rules": { - "foo-rule": { - "message": "Foo is falsy", - "given": "$.foo", - "then": { - "function": "falsy" - } - } - } -} diff --git a/packages/core/src/ruleset/__tests__/__fixtures__/foo-ruleset.json b/packages/core/src/ruleset/__tests__/__fixtures__/foo-ruleset.json deleted file mode 100644 index f483ef52d..000000000 --- a/packages/core/src/ruleset/__tests__/__fixtures__/foo-ruleset.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "functions": [ - "foo.cjs" - ], - "rules": { - "foo-rule": { - "message": "should be OK", - "given": "$.info", - "then": { - "function": "foo.cjs" - } - } - } -} diff --git a/packages/core/src/ruleset/__tests__/__fixtures__/invalid-ruleset.json b/packages/core/src/ruleset/__tests__/__fixtures__/invalid-ruleset.json deleted file mode 100644 index c885556db..000000000 --- a/packages/core/src/ruleset/__tests__/__fixtures__/invalid-ruleset.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "rules": { - "no-given-no-then": { - "message": "deliberetely invalid" - }, - "valid-rule": { - "message": "should be OK", - "given": "$.info", - "then": { - "function": "truthy" - } - }, - "rule-with-invalid-enum": { - "given": "$.info", - "then": { - "function": "truthy" - }, - "severity": "must not be a string", - "type": "some bs type value" - } - } -} diff --git a/packages/core/src/ruleset/__tests__/__fixtures__/ruleset-with-merge-keys.yaml b/packages/core/src/ruleset/__tests__/__fixtures__/ruleset-with-merge-keys.yaml deleted file mode 100644 index f10f02f51..000000000 --- a/packages/core/src/ruleset/__tests__/__fixtures__/ruleset-with-merge-keys.yaml +++ /dev/null @@ -1,20 +0,0 @@ -rules: - no-x-headers-request: &no-x-headers - description: "All 'HTTP' headers SHOULD NOT include 'X-' headers (https://tools.ietf.org/html/rfc6648)." - severity: warn - given: - - "$..parameters[?(@.in == 'header')].name" - message: |- - HTTP header '{{value}}' SHOULD NOT include 'X-' prefix in {{path}} - recommended: true - type: style - then: - function: pattern - functionOptions: - notMatch: "/^[xX]-/" - no-x-headers-response: - <<: *no-x-headers - given: - - $.[responses][*].headers.*~ - message: |- - HTTP header '{{value}}' SHOULD NOT include 'X-' prefix in {{path}} diff --git a/packages/core/src/ruleset/__tests__/__fixtures__/ruleset-with-missing-functions.json b/packages/core/src/ruleset/__tests__/__fixtures__/ruleset-with-missing-functions.json deleted file mode 100644 index 3573d2b49..000000000 --- a/packages/core/src/ruleset/__tests__/__fixtures__/ruleset-with-missing-functions.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rules": {}, - "functions": ["boo"] -} diff --git a/packages/core/src/ruleset/__tests__/__fixtures__/valid-flat-ruleset-2.json b/packages/core/src/ruleset/__tests__/__fixtures__/valid-flat-ruleset-2.json deleted file mode 100644 index 73b58dfbf..000000000 --- a/packages/core/src/ruleset/__tests__/__fixtures__/valid-flat-ruleset-2.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "rules": { - "valid-rule-2": { - "given": "$.info", - "then": { - "function": "truthy" - } - } - } -} diff --git a/packages/core/src/ruleset/__tests__/__fixtures__/valid-flat-ruleset.json b/packages/core/src/ruleset/__tests__/__fixtures__/valid-flat-ruleset.json deleted file mode 100644 index 21b4936bc..000000000 --- a/packages/core/src/ruleset/__tests__/__fixtures__/valid-flat-ruleset.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "rules": { - "valid-rule": { - "given": "$.info", - "then": { - "function": "truthy" - } - } - } -} diff --git a/packages/core/src/ruleset/__tests__/ruleset.test.ts b/packages/core/src/ruleset/__tests__/ruleset.test.ts index 4353ce3e0..bd23bd79f 100644 --- a/packages/core/src/ruleset/__tests__/ruleset.test.ts +++ b/packages/core/src/ruleset/__tests__/ruleset.test.ts @@ -6,7 +6,7 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { Ruleset } from '../ruleset'; import { RulesetDefinition } from '../types'; import { print } from './__helpers__/print'; -import { RulesetValidationError } from '../validation'; +import { RulesetValidationError } from '../validation/index'; import { isPlainObject } from '@stoplight/json'; import { Format } from '../format'; import { JSONSchema4, JSONSchema6, JSONSchema7 } from 'json-schema'; diff --git a/packages/core/src/ruleset/index.ts b/packages/core/src/ruleset/index.ts index e3ac2fb6e..6a91d8fa9 100644 --- a/packages/core/src/ruleset/index.ts +++ b/packages/core/src/ruleset/index.ts @@ -1,4 +1,4 @@ -export { assertValidRuleset, RulesetValidationError } from './validation'; +export { assertValidRuleset, RulesetValidationError } from './validation/index'; export { getDiagnosticSeverity } from './utils'; export { createRulesetFunction, SchemaDefinition as RulesetFunctionSchemaDefinition } from './rulesetFunction'; export { Format } from './format'; diff --git a/packages/core/src/ruleset/mergers/rules.ts b/packages/core/src/ruleset/mergers/rules.ts index 04396b5b0..fd6747737 100644 --- a/packages/core/src/ruleset/mergers/rules.ts +++ b/packages/core/src/ruleset/mergers/rules.ts @@ -1,5 +1,5 @@ import { Optional } from '@stoplight/types'; -import { assertValidRule } from '../validation'; +import { assertValidRule } from '../validation/assertions'; import { Rule } from '../rule'; import type { Ruleset } from '../ruleset'; import { FileRuleDefinition } from '../types'; diff --git a/packages/core/src/ruleset/meta/js-extensions.json b/packages/core/src/ruleset/meta/js-extensions.json new file mode 100644 index 000000000..1d497c334 --- /dev/null +++ b/packages/core/src/ruleset/meta/js-extensions.json @@ -0,0 +1,64 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "@stoplight/spectral-core/meta/extensions", + "$defs": { + "Extends": { + "$anchor": "extends", + "oneOf": [ + { + "$id": "ruleset", + "$ref": "ruleset.schema#", + "errorMessage": "must be a valid ruleset" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "ruleset" + }, + { + "type": "array", + "minItems": 2, + "additionalItems": false, + "items": [ + { + "$ref": "ruleset" + }, + { + "type": "string", + "enum": ["off", "recommended", "all"], + "errorMessage": "allowed types are \"off\", \"recommended\" and \"all\"" + } + ] + } + ] + } + } + ], + "errorMessage": "must be a valid ruleset" + }, + "Format": { + "$anchor": "format", + "spectral-runtime": "format", + "errorMessage": "must be a valid format" + }, + "Function": { + "$anchor": "function", + "spectral-runtime": "function", + "type": "object", + "properties": { + "function": true + }, + "required": ["function"] + }, + "Functions": { + "$anchor": "functions", + "not": {} + }, + "FunctionsDir": { + "$anchor": "functionsDir", + "not": {} + } + } +} diff --git a/packages/core/src/ruleset/meta/json-extensions.json b/packages/core/src/ruleset/meta/json-extensions.json new file mode 100644 index 000000000..b57698d83 --- /dev/null +++ b/packages/core/src/ruleset/meta/json-extensions.json @@ -0,0 +1,79 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "@stoplight/spectral-core/meta/extensions", + "$defs": { + "Extends": { + "$anchor": "extends", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 2, + "additionalItems": false, + "items": [ + { + "type": "string" + }, + { + "enum": ["all", "recommended", "off"], + "errorMessage": "allowed types are \"off\", \"recommended\" and \"all\"" + } + ] + } + ] + } + } + ] + }, + "Format": { + "$anchor": "format", + "enum": [ + "oas2", + "oas3", + "oas3.0", + "oas3.1", + "asyncapi2", + "json-schema", + "json-schema-loose", + "json-schema-draft4", + "json-schema-draft6", + "json-schema-draft7", + "json-schema-draft-2019-09", + "json-schema-2019-09", + "json-schema-draft-2020-12", + "json-schema-2020-12" + ], + "errorMessage": "must be a valid format" + }, + "Functions": { + "$anchor": "functions", + "type": "array", + "items": { + "type": "string" + } + }, + "FunctionsDir": { + "$anchor": "functionsDir", + "type": "string" + }, + "Function": { + "$anchor": "function", + "type": "object", + "properties": { + "function": { + "type": "string" + } + }, + "required": ["function"] + } + } +} diff --git a/packages/core/src/meta/rule.schema.json b/packages/core/src/ruleset/meta/rule.schema.json similarity index 94% rename from packages/core/src/meta/rule.schema.json rename to packages/core/src/ruleset/meta/rule.schema.json index d49fb758b..4372cacff 100644 --- a/packages/core/src/meta/rule.schema.json +++ b/packages/core/src/ruleset/meta/rule.schema.json @@ -13,9 +13,7 @@ } }, { - "type": "object", - "spectral-runtime": "ruleset-function", - "required": ["function"] + "$ref": "extensions#function" } ] }, diff --git a/packages/core/src/meta/ruleset.schema.json b/packages/core/src/ruleset/meta/ruleset.schema.json similarity index 85% rename from packages/core/src/meta/ruleset.schema.json rename to packages/core/src/ruleset/meta/ruleset.schema.json index d567e704d..553aa01fc 100644 --- a/packages/core/src/meta/ruleset.schema.json +++ b/packages/core/src/ruleset/meta/ruleset.schema.json @@ -9,49 +9,26 @@ "format": "url", "errorMessage": "must be a valid URL" }, - "description": { "type": "string" }, + "description": { + "type": "string" + }, "rules": { "type": "object", "additionalProperties": { "$ref": "rule.schema#" } }, + "functions": { + "$ref": "extensions#functions" + }, + "functionsDir": { + "$ref": "extensions#functionsDir" + }, "formats": { "$ref": "shared#formats" }, "extends": { - "oneOf": [ - { - "$ref": "#", - "errorMessage": "must be a valid ruleset" - }, - { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/properties/extends/oneOf/0" - }, - { - "type": "array", - "minItems": 2, - "additionalItems": false, - "items": [ - { - "$ref": "#" - }, - { - "type": "string", - "enum": ["off", "recommended", "all"], - "errorMessage": "allowed types are \"off\", \"recommended\" and \"all\"" - } - ] - } - ] - } - } - ], - "errorMessage": "must be a valid ruleset" + "$ref": "extensions#extends" }, "parserOptions": { "type": "object", diff --git a/packages/core/src/meta/shared.json b/packages/core/src/ruleset/meta/shared.json similarity index 89% rename from packages/core/src/meta/shared.json rename to packages/core/src/ruleset/meta/shared.json index 7ae2f4496..e46e507cf 100644 --- a/packages/core/src/meta/shared.json +++ b/packages/core/src/ruleset/meta/shared.json @@ -6,15 +6,10 @@ "$anchor": "formats", "type": "array", "items": { - "$ref": "#format" + "$ref": "extensions#format" }, "errorMessage": "must be an array of formats" }, - "Format": { - "$anchor": "format", - "spectral-runtime": "spectral-format", - "errorMessage": "must be a valid format" - }, "DiagnosticSeverity": { "enum": [-1, 0, 1, 2, 3] }, diff --git a/packages/core/src/ruleset/validation.ts b/packages/core/src/ruleset/validation.ts deleted file mode 100644 index 8df74f589..000000000 --- a/packages/core/src/ruleset/validation.ts +++ /dev/null @@ -1,129 +0,0 @@ -import Ajv, { _, ErrorObject } from 'ajv'; -import addFormats from 'ajv-formats'; -import addErrors from 'ajv-errors'; -import { isPlainObject } from '@stoplight/json'; -import { printPath, PrintStyle } from '@stoplight/spectral-runtime'; - -import * as ruleSchema from '../meta/rule.schema.json'; -import * as rulesetSchema from '../meta/ruleset.schema.json'; -import * as shared from '../meta/shared.json'; -import type { FileRuleDefinition, RuleDefinition, RulesetDefinition } from './types'; - -const message = _`'spectral-message'`; - -const ajv = new Ajv({ allErrors: true, strict: true, strictRequired: false, keywords: ['$anchor', 'x-internal'] }); -addFormats(ajv); -addErrors(ajv); -ajv.addKeyword({ - keyword: 'spectral-runtime', - schemaType: 'string', - error: { - message(ctx) { - return _`${ctx.data}[Symbol.for(${message})]`; - }, - }, - code(cxt) { - const { data } = cxt; - - switch (cxt.schema as unknown) { - case 'spectral-format': - cxt.fail(_`typeof ${data} !== "function"`); - break; - case 'spectral-function': - cxt.pass(_`typeof ${data}.function === "function"`); - cxt.pass( - _`(() => { try { ${data}.function.validator && ${data}.function.validator('functionOptions' in ${data} ? ${data} : null); } catch (e) { ${data}[${message}] = e.message } })()`, - ); - break; - } - }, -}); - -const validate = ajv.addSchema(ruleSchema).addSchema(shared).compile(rulesetSchema); - -export class RulesetValidationError extends Error { - constructor(public readonly message: string) { - super(message); - } -} - -const RULE_INSTANCE_PATH = /^\/rules\/[^/]+/; -const GENERIC_INSTANCE_PATH = /^\/(?:aliases|extends|overrides(?:\/\d+\/extends)?)/; - -class RulesetAjvValidationError extends RulesetValidationError { - constructor(public ruleset: Record, public errors: ErrorObject[]) { - super(RulesetAjvValidationError.serializeAjvErrors(ruleset, errors)); - } - - public static serializeAjvErrors(ruleset: Record, errors: ErrorObject[]): string { - const sortedErrors = [...errors] - .sort((errorA, errorB) => { - const diff = errorA.instancePath.length - errorB.instancePath.length; - return diff === 0 ? (errorA.keyword === 'errorMessage' && errorB.keyword !== 'errorMessage' ? -1 : 0) : diff; - }) - .filter((error, i, sortedErrors) => i === 0 || sortedErrors[i - 1].instancePath !== error.instancePath); - - const filteredErrors: ErrorObject[] = []; - - l: for (let i = 0; i < sortedErrors.length; i++) { - const error = sortedErrors[i]; - const prevError = i === 0 ? null : sortedErrors[i - 1]; - - if (GENERIC_INSTANCE_PATH.test(error.instancePath)) { - let x = 1; - while (i + x < sortedErrors.length) { - if ( - sortedErrors[i + x].instancePath.startsWith(error.instancePath) || - !GENERIC_INSTANCE_PATH.test(sortedErrors[i + x].instancePath) - ) { - continue l; - } - - x++; - } - } else if (prevError === null) { - filteredErrors.push(error); - continue; - } else { - const match = RULE_INSTANCE_PATH.exec(error.instancePath); - - if (match !== null && match[0] !== match.input && match[0] === prevError.instancePath) { - filteredErrors.pop(); - } - } - - filteredErrors.push(error); - } - - return filteredErrors - .map( - ({ message, instancePath }) => - `Error at ${printPath(instancePath.slice(1).split('/'), PrintStyle.Pointer)}: ${message ?? ''}`, - ) - .join('\n'); - } -} - -export function assertValidRuleset(ruleset: unknown): asserts ruleset is RulesetDefinition { - if (!isPlainObject(ruleset)) { - throw new Error('Provided ruleset is not an object'); - } - - if (!('rules' in ruleset) && !('extends' in ruleset) && !('overrides' in ruleset)) { - throw new RulesetValidationError('Ruleset must have rules or extends or overrides defined'); - } - - if (!validate(ruleset)) { - throw new RulesetAjvValidationError(ruleset, validate.errors ?? []); - } -} - -export function isValidRule(rule: FileRuleDefinition): rule is RuleDefinition { - return typeof rule === 'object' && rule !== null && !Array.isArray(rule) && ('given' in rule || 'then' in rule); -} - -export function assertValidRule(rule: FileRuleDefinition): asserts rule is RuleDefinition { - if (!isValidRule(rule)) { - throw new TypeError('Invalid rule'); - } -} diff --git a/packages/core/src/ruleset/validation/__tests__/__fixtures__/invalid-ruleset.ts b/packages/core/src/ruleset/validation/__tests__/__fixtures__/invalid-ruleset.ts new file mode 100644 index 000000000..a041af340 --- /dev/null +++ b/packages/core/src/ruleset/validation/__tests__/__fixtures__/invalid-ruleset.ts @@ -0,0 +1,24 @@ +import { truthy } from '@stoplight/spectral-functions'; + +export default { + rules: { + 'no-given-no-then': { + message: 'deliberately invalid', + }, + 'valid-rule': { + message: 'should be OK', + given: '$.info', + then: { + function: truthy, + }, + }, + 'rule-with-invalid-enum': { + given: '$.info', + then: { + function: truthy, + }, + severity: 'must not be a string', + type: 'some bs type value', + }, + }, +}; diff --git a/packages/core/src/ruleset/validation/__tests__/__fixtures__/valid-flat-ruleset.ts b/packages/core/src/ruleset/validation/__tests__/__fixtures__/valid-flat-ruleset.ts new file mode 100644 index 000000000..94f809ceb --- /dev/null +++ b/packages/core/src/ruleset/validation/__tests__/__fixtures__/valid-flat-ruleset.ts @@ -0,0 +1,12 @@ +import { truthy } from '@stoplight/spectral-functions'; + +export default { + rules: { + 'valid-rule': { + given: '$.info', + then: { + function: truthy, + }, + }, + }, +}; diff --git a/packages/core/src/ruleset/__tests__/validation.test.ts b/packages/core/src/ruleset/validation/__tests__/validation.test.ts similarity index 85% rename from packages/core/src/ruleset/__tests__/validation.test.ts rename to packages/core/src/ruleset/validation/__tests__/validation.test.ts index f87d22ea4..81e7715c5 100644 --- a/packages/core/src/ruleset/__tests__/validation.test.ts +++ b/packages/core/src/ruleset/validation/__tests__/validation.test.ts @@ -1,14 +1,16 @@ -import { schema, truthy } from '@stoplight/spectral-functions'; -import { Format } from '../format'; -import { assertValidRuleset, RulesetValidationError } from '../validation'; -import { RulesetDefinition, RulesetOverridesDefinition } from '../types'; -const invalidRuleset = require('./__fixtures__/invalid-ruleset.json'); -const validRuleset = require('./__fixtures__/valid-flat-ruleset.json'); +import { truthy } from '@stoplight/spectral-functions'; +import type { Format } from '../../format'; + +import { assertValidRuleset, RulesetValidationError } from '../index'; +import invalidRuleset from './__fixtures__/invalid-ruleset'; +import validRuleset from './__fixtures__/valid-flat-ruleset'; + +import { RulesetDefinition, RulesetOverridesDefinition } from '../../types'; const formatA: Format = () => false; const formatB: Format = () => false; -describe('Ruleset Validation', () => { +describe('JS Ruleset Validation', () => { it('given primitive type, throws', () => { expect(assertValidRuleset.bind(null, null)).toThrow('Provided ruleset is not an object'); expect(assertValidRuleset.bind(null, 2)).toThrow('Provided ruleset is not an object'); @@ -144,22 +146,6 @@ Error at #/rules/rule-with-invalid-enum/severity: the value has to be one of: 0, ).not.toThrow(); }); - it('recognizes string extends syntax', () => { - expect( - assertValidRuleset.bind(null, { - rules: { - foo: { - given: '$', - then: { - function: schema, - functionOptions: {}, - }, - }, - }, - }), - ).not.toThrow(); - }); - it.each<[unknown, string]>([ [[[{ rules: {} }, 'test']], `Error at #/extends/0/1: allowed types are "off", "recommended" and "all"`], [ @@ -655,3 +641,120 @@ Error at #/parserOptions/incompatibleValues: the value has to be one of: 0, 1, 2 }); }); }); + +// we only check the most notable differences here, since the rest of the validation process is common to both JS and JSON +describe('JSON Ruleset Validation', () => { + it('recognizes valid array-ish extends syntax', () => { + expect( + assertValidRuleset.bind( + null, + { + extends: [['rulesetA', 'off'], 'rulesetB'], + rules: {}, + }, + 'json', + ), + ).not.toThrow(); + }); + + it.each<[unknown, string]>([ + [[['test', 'test']], `Error at #/extends/0/1: allowed types are "off", "recommended" and "all"`], + [ + [['bar', 'test'], {}], + `Error at #/extends/1: must be string +Error at #/extends/0/1: allowed types are "off", "recommended" and "all"`, + ], + ])('recognizes invalid array-ish extends syntax %p', (_extends, message) => { + expect( + assertValidRuleset.bind( + null, + { + extends: _extends, + }, + 'json', + ), + ).toThrow(new RulesetValidationError(message)); + }); + + it('recognizes valid ruleset formats syntax', () => { + expect( + assertValidRuleset.bind( + null, + { + formats: ['oas2'], + rules: {}, + }, + 'json', + ), + ).not.toThrow(); + }); + + it.each([ + [ + [2, 'a'], + `Error at #/formats/0: must be a valid format +Error at #/formats/1: must be a valid format`, + ], + [2, 'Error at #/formats: must be an array of formats'], + [[''], 'Error at #/formats/0: must be a valid format'], + ])('recognizes invalid ruleset %p formats syntax', (formats, error) => { + expect( + assertValidRuleset.bind( + null, + { + formats, + rules: {}, + }, + 'json', + ), + ).toThrow(new RulesetValidationError(error)); + }); + + it('recognizes valid rule formats syntax', () => { + expect( + assertValidRuleset.bind( + null, + { + formats: ['json-schema-loose'], + rules: { + rule: { + given: '$.info', + then: { + function: 'truthy', + }, + formats: ['oas2'], + }, + }, + }, + 'json', + ), + ).not.toThrow(); + }); + + it.each([ + [ + [2, 'a'], + `Error at #/rules/rule/formats/0: must be a valid format +Error at #/rules/rule/formats/1: must be a valid format`, + ], + [2, 'Error at #/rules/rule/formats: must be an array of formats'], + ])('recognizes invalid rule %p formats syntax', (formats, error) => { + expect( + assertValidRuleset.bind( + null, + { + rules: { + rule: { + given: '$.info', + then: { + function: 'truthy', + }, + formats, + }, + }, + }, + 'json', + ), + ).toThrow(new RulesetValidationError(error)); + }); +}); diff --git a/packages/core/src/ruleset/validation/ajv.ts b/packages/core/src/ruleset/validation/ajv.ts new file mode 100644 index 000000000..88fecdadc --- /dev/null +++ b/packages/core/src/ruleset/validation/ajv.ts @@ -0,0 +1,66 @@ +import Ajv, { _, ValidateFunction } from 'ajv'; +import addFormats from 'ajv-formats'; +import addErrors from 'ajv-errors'; +import * as ruleSchema from '../meta/rule.schema.json'; +import * as shared from '../meta/shared.json'; +import * as rulesetSchema from '../meta/ruleset.schema.json'; +import * as jsExtensions from '../meta/js-extensions.json'; +import * as jsonExtensions from '../meta/json-extensions.json'; + +const message = _`'spectral-message'`; + +const validators: { [key in 'js' | 'json']: null | ValidateFunction } = { + js: null, + json: null, +}; + +export function createValidator(format: 'js' | 'json'): ValidateFunction { + const existingValidator = validators[format]; + if (existingValidator !== null) { + return existingValidator; + } + + const ajv = new Ajv({ + allErrors: true, + strict: true, + strictRequired: false, + keywords: ['$anchor'], + schemas: [ruleSchema, shared], + }); + addFormats(ajv); + addErrors(ajv); + ajv.addKeyword({ + keyword: 'spectral-runtime', + schemaType: 'string', + error: { + message(ctx) { + return _`${ctx.data}[Symbol.for(${message})]`; + }, + }, + code(cxt) { + const { data } = cxt; + + switch (cxt.schema as unknown) { + case 'format': + cxt.fail(_`typeof ${data} !== "function"`); + break; + case 'ruleset-function': + cxt.pass(_`typeof ${data}.function === "function"`); + cxt.pass( + _`(() => { try { ${data}.function.validator && ${data}.function.validator('functionOptions' in ${data} ? ${data} : null); } catch (e) { ${data}[${message}] = e.message } })()`, + ); + break; + } + }, + }); + + if (format === 'js') { + ajv.addSchema(jsExtensions); + } else { + ajv.addSchema(jsonExtensions); + } + + const validator = ajv.compile(rulesetSchema); + validators[format] = validator; + return validator; +} diff --git a/packages/core/src/ruleset/validation/assertions.ts b/packages/core/src/ruleset/validation/assertions.ts new file mode 100644 index 000000000..f8702feb9 --- /dev/null +++ b/packages/core/src/ruleset/validation/assertions.ts @@ -0,0 +1,33 @@ +import { isPlainObject } from '@stoplight/json'; +import { createValidator } from './ajv'; +import { RulesetAjvValidationError, RulesetValidationError } from './errors'; +import type { FileRuleDefinition, RuleDefinition, RulesetDefinition } from '../types'; + +export function assertValidRuleset( + ruleset: unknown, + format: 'js' | 'json' = 'js', +): asserts ruleset is RulesetDefinition { + if (!isPlainObject(ruleset)) { + throw new Error('Provided ruleset is not an object'); + } + + if (!('rules' in ruleset) && !('extends' in ruleset) && !('overrides' in ruleset)) { + throw new RulesetValidationError('Ruleset must have rules or extends or overrides defined'); + } + + const validate = createValidator(format); + + if (!validate(ruleset)) { + throw new RulesetAjvValidationError(ruleset, validate.errors ?? []); + } +} + +export function isValidRule(rule: FileRuleDefinition): rule is RuleDefinition { + return typeof rule === 'object' && rule !== null && !Array.isArray(rule) && ('given' in rule || 'then' in rule); +} + +export function assertValidRule(rule: FileRuleDefinition): asserts rule is RuleDefinition { + if (!isValidRule(rule)) { + throw new TypeError('Invalid rule'); + } +} diff --git a/packages/core/src/ruleset/validation/errors.ts b/packages/core/src/ruleset/validation/errors.ts new file mode 100644 index 000000000..3f3d120ef --- /dev/null +++ b/packages/core/src/ruleset/validation/errors.ts @@ -0,0 +1,65 @@ +import { ErrorObject } from 'ajv'; +import { printPath, PrintStyle } from '@stoplight/spectral-runtime'; + +export class RulesetValidationError extends Error { + constructor(public readonly message: string) { + super(message); + } +} + +const RULE_INSTANCE_PATH = /^\/rules\/[^/]+/; +const GENERIC_INSTANCE_PATH = /^\/(?:aliases|extends|overrides(?:\/\d+\/extends)?)/; + +export class RulesetAjvValidationError extends RulesetValidationError { + constructor(public ruleset: Record, public errors: ErrorObject[]) { + super(RulesetAjvValidationError.serializeAjvErrors(ruleset, errors)); + } + + public static serializeAjvErrors(ruleset: Record, errors: ErrorObject[]): string { + const sortedErrors = [...errors] + .sort((errorA, errorB) => { + const diff = errorA.instancePath.length - errorB.instancePath.length; + return diff === 0 ? (errorA.keyword === 'errorMessage' && errorB.keyword !== 'errorMessage' ? -1 : 0) : diff; + }) + .filter((error, i, sortedErrors) => i === 0 || sortedErrors[i - 1].instancePath !== error.instancePath); + + const filteredErrors: ErrorObject[] = []; + + l: for (let i = 0; i < sortedErrors.length; i++) { + const error = sortedErrors[i]; + const prevError = i === 0 ? null : sortedErrors[i - 1]; + + if (GENERIC_INSTANCE_PATH.test(error.instancePath)) { + let x = 1; + while (i + x < sortedErrors.length) { + if ( + sortedErrors[i + x].instancePath.startsWith(error.instancePath) || + !GENERIC_INSTANCE_PATH.test(sortedErrors[i + x].instancePath) + ) { + continue l; + } + + x++; + } + } else if (prevError === null) { + filteredErrors.push(error); + continue; + } else { + const match = RULE_INSTANCE_PATH.exec(error.instancePath); + + if (match !== null && match[0] !== match.input && match[0] === prevError.instancePath) { + filteredErrors.pop(); + } + } + + filteredErrors.push(error); + } + + return filteredErrors + .map( + ({ message, instancePath }) => + `Error at ${printPath(instancePath.slice(1).split('/'), PrintStyle.Pointer)}: ${message ?? ''}`, + ) + .join('\n'); + } +} diff --git a/packages/core/src/ruleset/validation/index.ts b/packages/core/src/ruleset/validation/index.ts new file mode 100644 index 000000000..b7eb566c4 --- /dev/null +++ b/packages/core/src/ruleset/validation/index.ts @@ -0,0 +1,2 @@ +export { RulesetValidationError } from './errors'; +export { assertValidRuleset } from './assertions'; From 7000ac2962026b264eb285a73961a8eb93780f64 Mon Sep 17 00:00:00 2001 From: stoplight-bot Date: Mon, 18 Apr 2022 12:14:04 +0000 Subject: [PATCH 03/19] chore(release): 1.12.0 [skip ci] # [@stoplight/spectral-core-v1.12.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-core-v1.11.1...@stoplight/spectral-core-v1.12.0) (2022-04-18) ### Features * **core:** support JSON ruleset validation ([#2062](https://github.com/stoplightio/spectral/issues/2062)) ([aeb7d5b](https://github.com/stoplightio/spectral/commit/aeb7d5b842741d85229fb2c8575ae2c58c8dfbb8)) --- packages/core/CHANGELOG.md | 7 +++++++ packages/core/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index aefa7c6f1..97fa437b3 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@stoplight/spectral-core-v1.12.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-core-v1.11.1...@stoplight/spectral-core-v1.12.0) (2022-04-18) + + +### Features + +* **core:** support JSON ruleset validation ([#2062](https://github.com/stoplightio/spectral/issues/2062)) ([aeb7d5b](https://github.com/stoplightio/spectral/commit/aeb7d5b842741d85229fb2c8575ae2c58c8dfbb8)) + # [@stoplight/spectral-core-v1.11.1](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-core-v1.11.0...@stoplight/spectral-core-v1.11.1) (2022-03-16) diff --git a/packages/core/package.json b/packages/core/package.json index da36979a3..d58d697f5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/spectral-core", - "version": "1.11.1", + "version": "1.12.0", "sideEffects": false, "homepage": "https://github.com/stoplightio/spectral", "bugs": "https://github.com/stoplightio/spectral/issues", From 99c3c8f65caa38de35276a62dbac4735977e967a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Wed, 20 Apr 2022 10:08:21 +0200 Subject: [PATCH 04/19] docs(repo): explain $refs caveat with overrides (#2127) --- docs/guides/4-custom-rulesets.md | 68 ++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/docs/guides/4-custom-rulesets.md b/docs/guides/4-custom-rulesets.md index 527a850a7..0bb994b77 100644 --- a/docs/guides/4-custom-rulesets.md +++ b/docs/guides/4-custom-rulesets.md @@ -454,3 +454,71 @@ overrides: ``` In the event of multiple matches, the order of definition takes place, with the last one having the higher priority. + +### Caveats + +Please bear in mind that overrides are only applied to the _root_ documents. If your documents have any external dependencies, i.e. $refs, the overrides won't apply. + +**Example:** + +Given the following 2 YAML documents + +```yaml +# my-document.yaml +openapi: "3.1.0" +paths: {} +components: + schemas: + User: + $ref: "./User.yaml" +``` + +```yaml +# User.yaml +title: "" +type: object +properties: + id: + type: string +required: + - id +``` + +and the ruleset below + +```json +{ + "rules": { + "empty-title-property": { + "message": "Title must not be empty", + "given": "$..title", + "then": { + "function": "truthy" + } + } + }, + "overrides": [ + { + "files": ["User.yaml"], + "rules": { + "empty-title-property": "off" + } + } + ] +} +``` + +running `spectral lint my-document.yaml` will result in + +``` +/project/User.yaml + 1:8 warning empty-title-property Title must not be empty title + +✖ 1 problem (0 errors, 1 warning, 0 infos, 0 hints) +``` + +while executing `spectral lint User.yaml` will output + +``` +No results with a severity of 'error' or higher found! +``` From 6def6be4bc3f318c8e93d2e1c0df2ff1b803a178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Wed, 27 Apr 2022 07:21:19 +0200 Subject: [PATCH 05/19] fix(core): redeclared rules should always be re-enabled (#2138) --- .../__fixtures__/severity/off-redeclared.ts | 15 +++++++++++++++ .../core/src/ruleset/__tests__/ruleset.test.ts | 12 ++++++++++++ packages/core/src/ruleset/mergers/rules.ts | 5 ++++- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/ruleset/__tests__/__fixtures__/severity/off-redeclared.ts diff --git a/packages/core/src/ruleset/__tests__/__fixtures__/severity/off-redeclared.ts b/packages/core/src/ruleset/__tests__/__fixtures__/severity/off-redeclared.ts new file mode 100644 index 000000000..b9c256a56 --- /dev/null +++ b/packages/core/src/ruleset/__tests__/__fixtures__/severity/off-redeclared.ts @@ -0,0 +1,15 @@ +import { RulesetDefinition } from '@stoplight/spectral-core'; +import shared from './shared'; +import { truthy } from '@stoplight/spectral-functions/src'; + +export default { + extends: [[shared, 'off']], + rules: { + 'overridable-rule': { + given: '$.foo', + then: { + function: truthy, + }, + }, + }, +} as RulesetDefinition; diff --git a/packages/core/src/ruleset/__tests__/ruleset.test.ts b/packages/core/src/ruleset/__tests__/ruleset.test.ts index bd23bd79f..24d8bdba0 100644 --- a/packages/core/src/ruleset/__tests__/ruleset.test.ts +++ b/packages/core/src/ruleset/__tests__/ruleset.test.ts @@ -68,6 +68,18 @@ describe('Ruleset', () => { expect(getEnabledRules(rules)).toEqual(['overridable-rule']); }); + it('given ruleset with extends set to off, should disable all rules but explicitly redeclared', async () => { + const { rules } = await loadRuleset(import('./__fixtures__/severity/off-redeclared')); + expect(Object.keys(rules)).toEqual([ + 'description-matches-stoplight', + 'title-matches-stoplight', + 'contact-name-matches-stoplight', + 'overridable-rule', + ]); + + expect(getEnabledRules(rules)).toEqual(['overridable-rule']); + }); + it('given nested extends with severity set to off', async () => { const { rules } = await loadRuleset(import('./__fixtures__/severity/off-proxy')); expect(Object.keys(rules)).toEqual([ diff --git a/packages/core/src/ruleset/mergers/rules.ts b/packages/core/src/ruleset/mergers/rules.ts index fd6747737..5140a3e9c 100644 --- a/packages/core/src/ruleset/mergers/rules.ts +++ b/packages/core/src/ruleset/mergers/rules.ts @@ -39,7 +39,10 @@ export function mergeRule( break; case 'object': if (existingRule !== void 0) { - Object.assign(existingRule, rule, { owner: existingRule.owner }); + Object.assign(existingRule, rule, { + enabled: true, + owner: existingRule.owner, + }); } else { assertValidRule(rule); return new Rule(name, rule, ruleset); From ec08efee42fb1b94ee5b5aeae3d82e2809881e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Wed, 27 Apr 2022 14:07:10 +0200 Subject: [PATCH 06/19] chore(core): improve ruleset typings (#2132) --- packages/core/package.json | 13 ++++--- .../src/ruleset/__tests__/ruleset.test.ts | 26 ++++++------- .../{utils/formatsSet.ts => formats.ts} | 4 +- .../{rulesetFunction.ts => function.ts} | 0 packages/core/src/ruleset/index.ts | 8 ++-- packages/core/src/ruleset/rule.ts | 13 ++++--- packages/core/src/ruleset/ruleset.ts | 27 ++++++------- packages/core/src/ruleset/types.ts | 21 ++++++---- packages/core/src/ruleset/utils/index.ts | 1 - tsconfig.build.json | 4 +- yarn.lock | 38 ++++++++++++------- 11 files changed, 86 insertions(+), 69 deletions(-) rename packages/core/src/ruleset/{utils/formatsSet.ts => formats.ts} (61%) rename packages/core/src/ruleset/{rulesetFunction.ts => function.ts} (100%) delete mode 100644 packages/core/src/ruleset/utils/index.ts diff --git a/packages/core/package.json b/packages/core/package.json index d58d697f5..b93c49a2e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -19,11 +19,15 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "require": "./dist/index.js" + "default": "./dist/index.js" + }, + "./ruleset": { + "types": "./dist/ruleset/index.d.ts", + "default": "./dist/ruleset/index.js" }, "./ruleset/validation": { "types": "./dist/ruleset/validation/index.d.ts", - "require": "./dist/ruleset/validation/index.js" + "default": "./dist/ruleset/validation/index.js" } }, "engines": { @@ -40,12 +44,12 @@ "@stoplight/spectral-parsers": "^1.0.0", "@stoplight/spectral-ref-resolver": "^1.0.0", "@stoplight/spectral-runtime": "^1.0.0", - "@stoplight/types": "12.3.0", + "@stoplight/types": "13.0.0", + "@types/json-schema": "^7.0.7", "ajv": "^8.6.0", "ajv-errors": "~3.0.0", "ajv-formats": "~2.1.0", "blueimp-md5": "2.18.0", - "json-schema": "0.4.0", "jsonpath-plus": "6.0.1", "lodash": "~4.17.21", "lodash.topath": "^4.5.2", @@ -60,7 +64,6 @@ "@stoplight/spectral-functions": "*", "@stoplight/spectral-parsers": "*", "@stoplight/yaml": "^4.2.2", - "@types/json-schema": "^7.0.7", "@types/minimatch": "^3.0.5", "@types/treeify": "^1.0.0", "nock": "^13.1.0", diff --git a/packages/core/src/ruleset/__tests__/ruleset.test.ts b/packages/core/src/ruleset/__tests__/ruleset.test.ts index 24d8bdba0..2fdc3fccd 100644 --- a/packages/core/src/ruleset/__tests__/ruleset.test.ts +++ b/packages/core/src/ruleset/__tests__/ruleset.test.ts @@ -9,8 +9,8 @@ import { print } from './__helpers__/print'; import { RulesetValidationError } from '../validation/index'; import { isPlainObject } from '@stoplight/json'; import { Format } from '../format'; -import { JSONSchema4, JSONSchema6, JSONSchema7 } from 'json-schema'; -import { FormatsSet } from '../utils/formatsSet'; +import type { JSONSchema4, JSONSchema6, JSONSchema7 } from 'json-schema'; +import { Formats } from '../formats'; async function loadRuleset(mod: Promise<{ default: RulesetDefinition }>, source?: string): Promise { return new Ruleset((await mod).default, { source }); @@ -1390,24 +1390,22 @@ describe('Ruleset', () => { }, }); - expect(ruleset.rules['valid-id'].getGivenForFormats(new FormatsSet([draft4]))).toStrictEqual(['$..id']); - expect(ruleset.rules['valid-id'].getGivenForFormats(new FormatsSet([draft6]))).toStrictEqual(['$..$id']); - expect(ruleset.rules['valid-id'].getGivenForFormats(new FormatsSet([draft7]))).toStrictEqual(['$..$id']); - expect(ruleset.rules['valid-id'].getGivenForFormats(new FormatsSet([draft6, draft7]))).toStrictEqual([ - '$..$id', - ]); + expect(ruleset.rules['valid-id'].getGivenForFormats(new Formats([draft4]))).toStrictEqual(['$..id']); + expect(ruleset.rules['valid-id'].getGivenForFormats(new Formats([draft6]))).toStrictEqual(['$..$id']); + expect(ruleset.rules['valid-id'].getGivenForFormats(new Formats([draft7]))).toStrictEqual(['$..$id']); + expect(ruleset.rules['valid-id'].getGivenForFormats(new Formats([draft6, draft7]))).toStrictEqual(['$..$id']); - expect(ruleset.rules['valid-parameter'].getGivenForFormats(new FormatsSet([oas2]))).toStrictEqual([ + expect(ruleset.rules['valid-parameter'].getGivenForFormats(new Formats([oas2]))).toStrictEqual([ '$.parameters[*]', '$.paths[*].parameters[?(@ && !@.$ref)]', '$.paths[*][get,put,post,delete,options,head,patch,trace].parameters[?(@ && !@.$ref)]', ]); - expect(ruleset.rules['valid-parameter'].getGivenForFormats(new FormatsSet([oas3]))).toStrictEqual([ + expect(ruleset.rules['valid-parameter'].getGivenForFormats(new Formats([oas3]))).toStrictEqual([ '$.components.parameters[*]', '$.paths[*].parameters[?(@ && !@.$ref)]', '$.paths[*][get,put,post,delete,options,head,patch,trace].parameters[?(@ && !@.$ref)]', ]); - expect(ruleset.rules['valid-parameter'].getGivenForFormats(new FormatsSet([oas2, oas3]))).toStrictEqual([ + expect(ruleset.rules['valid-parameter'].getGivenForFormats(new Formats([oas2, oas3]))).toStrictEqual([ '$.components.parameters[*]', '$.paths[*].parameters[?(@ && !@.$ref)]', '$.paths[*][get,put,post,delete,options,head,patch,trace].parameters[?(@ && !@.$ref)]', @@ -1454,7 +1452,7 @@ describe('Ruleset', () => { }, }); - expect(() => ruleset.rules['valid-header'].getGivenForFormats(new FormatsSet([oas3]))).toThrowError( + expect(() => ruleset.rules['valid-header'].getGivenForFormats(new Formats([oas3]))).toThrowError( ReferenceError( 'Alias "HeaderObject" is circular. Resolution stack: HeaderObject -> HeaderObjects -> Components -> HeaderObject', ), @@ -1491,8 +1489,8 @@ describe('Ruleset', () => { }, }); - expect(ruleset.rules['valid-id'].getGivenForFormats(new FormatsSet([draft7]))).toStrictEqual([]); - expect(ruleset.rules['valid-id'].getGivenForFormats(new FormatsSet([]))).toStrictEqual([]); + expect(ruleset.rules['valid-id'].getGivenForFormats(new Formats([draft7]))).toStrictEqual([]); + expect(ruleset.rules['valid-id'].getGivenForFormats(new Formats([]))).toStrictEqual([]); }); it('should be serializable', () => { diff --git a/packages/core/src/ruleset/utils/formatsSet.ts b/packages/core/src/ruleset/formats.ts similarity index 61% rename from packages/core/src/ruleset/utils/formatsSet.ts rename to packages/core/src/ruleset/formats.ts index 0be49eafb..c8a9063cc 100644 --- a/packages/core/src/ruleset/utils/formatsSet.ts +++ b/packages/core/src/ruleset/formats.ts @@ -1,10 +1,10 @@ -import type { Format } from '../format'; +import type { Format } from './format'; function printFormat(format: Format): string { return format.displayName ?? format.name; } -export class FormatsSet extends Set { +export class Formats extends Set { public toJSON(): string[] { return Array.from(this).map(printFormat); } diff --git a/packages/core/src/ruleset/rulesetFunction.ts b/packages/core/src/ruleset/function.ts similarity index 100% rename from packages/core/src/ruleset/rulesetFunction.ts rename to packages/core/src/ruleset/function.ts diff --git a/packages/core/src/ruleset/index.ts b/packages/core/src/ruleset/index.ts index 6a91d8fa9..50addc0e8 100644 --- a/packages/core/src/ruleset/index.ts +++ b/packages/core/src/ruleset/index.ts @@ -1,7 +1,7 @@ export { assertValidRuleset, RulesetValidationError } from './validation/index'; -export { getDiagnosticSeverity } from './utils'; -export { createRulesetFunction, SchemaDefinition as RulesetFunctionSchemaDefinition } from './rulesetFunction'; +export { getDiagnosticSeverity } from './utils/severity'; +export { createRulesetFunction, SchemaDefinition as RulesetFunctionSchemaDefinition } from './function'; export { Format } from './format'; export { RulesetDefinition, RuleDefinition, ParserOptions, HumanReadableDiagnosticSeverity } from './types'; -export { Ruleset } from './ruleset'; -export { Rule } from './rule'; +export { Ruleset, StringifiedRuleset } from './ruleset'; +export { Rule, StringifiedRule } from './rule'; diff --git a/packages/core/src/ruleset/rule.ts b/packages/core/src/ruleset/rule.ts index a97d1b03b..d7a73608a 100644 --- a/packages/core/src/ruleset/rule.ts +++ b/packages/core/src/ruleset/rule.ts @@ -13,9 +13,10 @@ import type { RuleDefinition, RulesetAliasesDefinition, RulesetScopedAliasDefinition, + Stringifable, } from './types'; import { minimatch } from './utils/minimatch'; -import { FormatsSet } from './utils/formatsSet'; +import { Formats } from './formats'; import { isSimpleAliasDefinition } from './utils/guards'; const ALIAS = /^#([A-Za-z0-9_-]+)/; @@ -25,7 +26,7 @@ export interface IRule { message: string | null; severity: DiagnosticSeverity; resolved: boolean; - formats: Set | null; + formats: Formats | null; enabled: boolean; recommended: boolean; documentationUrl: string | null; @@ -35,7 +36,7 @@ export interface IRule { export type StringifiedRule = Omit & { name: string; - formats: FormatsSet | null; + formats: string[] | null; then: (Pick & { function: string; functionOptions?: string })[]; owner: number; }; @@ -45,7 +46,7 @@ export class Rule implements IRule { public message: string | null; #severity!: DiagnosticSeverity; public resolved: boolean; - public formats: FormatsSet | null; + public formats: Formats | null; #enabled: boolean; public recommended: boolean; public documentationUrl: string | null; @@ -64,7 +65,7 @@ export class Rule implements IRule { this.documentationUrl = definition.documentationUrl ?? null; this.severity = definition.severity; this.resolved = definition.resolved !== false; - this.formats = 'formats' in definition ? new FormatsSet(definition.formats) : null; + this.formats = 'formats' in definition ? new Formats(definition.formats) : null; this.then = definition.then; this.given = definition.given; } @@ -241,7 +242,7 @@ export class Rule implements IRule { return new Rule(this.name, this.definition, this.owner); } - public toJSON(): StringifiedRule { + public toJSON(): Stringifable { return { name: this.name, recommended: this.recommended, diff --git a/packages/core/src/ruleset/ruleset.ts b/packages/core/src/ruleset/ruleset.ts index 0dbf80c0b..87f11718a 100644 --- a/packages/core/src/ruleset/ruleset.ts +++ b/packages/core/src/ruleset/ruleset.ts @@ -1,20 +1,21 @@ import { dirname, relative } from '@stoplight/path'; +import { isPlainObject, extractPointerFromRef, extractSourceFromRef } from '@stoplight/json'; +import { DiagnosticSeverity } from '@stoplight/types'; import { minimatch } from './utils/minimatch'; import { Rule, StringifiedRule } from './rule'; -import { +import type { FileRulesetSeverityDefinition, ParserOptions, RulesetAliasesDefinition, RulesetDefinition, RulesetOverridesDefinition, + Stringifable, } from './types'; -import { assertValidRuleset } from './validation'; +import { assertValidRuleset } from './validation/index'; import { mergeRule } from './mergers/rules'; import { DEFAULT_PARSER_OPTIONS, getDiagnosticSeverity } from '..'; import { mergeRulesets } from './mergers/rulesets'; -import { isPlainObject, extractPointerFromRef, extractSourceFromRef } from '@stoplight/json'; -import { DiagnosticSeverity } from '@stoplight/types'; -import { FormatsSet } from './utils/formatsSet'; +import { Formats } from './formats'; import { isSimpleAliasDefinition } from './utils/guards'; const STACK_SYMBOL = Symbol('@stoplight/spectral/ruleset/#stack'); @@ -33,7 +34,7 @@ export type StringifiedRuleset = { extends: StringifiedRuleset[] | null; source: string | null; aliases: RulesetAliasesDefinition | null; - formats: FormatsSet | null; + formats: Formats | null; rules: Record; overrides: RulesetOverridesDefinition | null; parserOptions: ParserOptions; @@ -43,7 +44,7 @@ export class Ruleset { public readonly id = SEED++; protected readonly extends: Ruleset[] | null; - public readonly formats = new FormatsSet(); + public readonly formats = new Formats(); public readonly overrides: RulesetOverridesDefinition | null; public readonly aliases: RulesetAliasesDefinition | null; public readonly hasComplexAliases: boolean; @@ -86,7 +87,7 @@ export class Ruleset { hasComplexAliases = true; const targets = value.targets.map(target => ({ - formats: new FormatsSet(target.formats), + formats: new Formats(target.formats), given: target.given, })); @@ -286,10 +287,9 @@ export class Ruleset { this.formats.add(format); } } else if (rule.owner !== this) { - rule.formats = - rule.owner.definition.formats === void 0 ? null : new FormatsSet(rule.owner.definition.formats); + rule.formats = rule.owner.definition.formats === void 0 ? null : new Formats(rule.owner.definition.formats); } else if (this.definition.formats !== void 0) { - rule.formats = new FormatsSet(this.definition.formats); + rule.formats = new Formats(this.definition.formats); } if (this.definition.documentationUrl !== void 0 && rule.documentationUrl === null) { @@ -309,10 +309,7 @@ export class Ruleset { return DEFAULT_RULESET_FILE.test(uri); } - public toJSON(): Omit & { - extends: Ruleset['extends']; - rules: Ruleset['rules']; - } { + public toJSON(): Stringifable { return { id: this.id, extends: this.extends, diff --git a/packages/core/src/ruleset/types.ts b/packages/core/src/ruleset/types.ts index f9882ee94..381a18d10 100644 --- a/packages/core/src/ruleset/types.ts +++ b/packages/core/src/ruleset/types.ts @@ -1,7 +1,7 @@ -import { DiagnosticSeverity } from '@stoplight/types'; -import { Format } from './format'; -import { RulesetFunction, RulesetFunctionWithValidator } from '../types'; -import { FormatsSet } from './utils/formatsSet'; +import type { DiagnosticSeverity } from '@stoplight/types'; +import type { Format } from './format'; +import type { RulesetFunction, RulesetFunctionWithValidator } from '../types'; +import type { Formats } from './formats'; export type HumanReadableDiagnosticSeverity = 'error' | 'warn' | 'info' | 'hint' | 'off'; export type FileRuleSeverityDefinition = DiagnosticSeverity | HumanReadableDiagnosticSeverity | boolean; @@ -17,7 +17,7 @@ export type ParserOptions = { export type RuleDefinition = { type?: 'validation' | 'style'; - formats?: Format[]; + formats?: Formats | Format[]; documentationUrl?: string; @@ -81,7 +81,7 @@ export type RulesetOverridesDefinition = ReadonlyArray<{ files: string[] } & Rul export type RulesetScopedAliasDefinition = { description?: string; targets: { - formats: FormatsSet | Format[]; + formats: Formats | Format[]; given: string[]; }[]; }; @@ -92,7 +92,7 @@ export type RulesetDefinition = Readonly< { documentationUrl?: string; description?: string; - formats?: FormatsSet | Format[]; + formats?: Formats | Format[]; parserOptions?: Partial; overrides?: RulesetOverridesDefinition; aliases?: RulesetAliasesDefinition; @@ -112,3 +112,10 @@ export type RulesetDefinition = Readonly< } > >; + +// eslint-disable-next-line @typescript-eslint/ban-types +export type Stringifable = T extends object + ? { + [P in keyof T]: Stringifable | { toJSON?(): Stringifable }; + } + : T; diff --git a/packages/core/src/ruleset/utils/index.ts b/packages/core/src/ruleset/utils/index.ts deleted file mode 100644 index f2f24e222..000000000 --- a/packages/core/src/ruleset/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './severity'; diff --git a/tsconfig.build.json b/tsconfig.build.json index b5210fefe..23d50a91c 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,7 +1,9 @@ { "include": [], "extends": "./tsconfig.json", - "composite": true, + "compilerOptions": { + "composite": true + }, "references": [ { "path": "./packages/cli/tsconfig.build.json" diff --git a/yarn.lock b/yarn.lock index f75cf7ec3..c222a3281 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2389,7 +2389,7 @@ __metadata: "@stoplight/spectral-parsers": "*" "@stoplight/spectral-ref-resolver": ^1.0.0 "@stoplight/spectral-runtime": ^1.0.0 - "@stoplight/types": 12.3.0 + "@stoplight/types": 13.0.0 "@stoplight/yaml": ^4.2.2 "@types/json-schema": ^7.0.7 "@types/minimatch": ^3.0.5 @@ -2398,7 +2398,6 @@ __metadata: ajv-errors: ~3.0.0 ajv-formats: ~2.1.0 blueimp-md5: 2.18.0 - json-schema: 0.4.0 jsonpath-plus: 6.0.1 lodash: ~4.17.21 lodash.topath: ^4.5.2 @@ -2559,7 +2558,7 @@ __metadata: languageName: unknown linkType: soft -"@stoplight/types@npm:12.3.0, @stoplight/types@npm:^12.0.0, @stoplight/types@npm:^12.3.0": +"@stoplight/types@npm:12.3.0": version: 12.3.0 resolution: "@stoplight/types@npm:12.3.0" dependencies: @@ -2569,6 +2568,26 @@ __metadata: languageName: node linkType: hard +"@stoplight/types@npm:13.0.0": + version: 13.0.0 + resolution: "@stoplight/types@npm:13.0.0" + dependencies: + "@types/json-schema": ^7.0.4 + utility-types: ^3.10.0 + checksum: 40a3995fe4f654b9761908eb41b4369d22241220e96aa0f3ace4541c160d995ddd52dee32fa574a87d7ba3f573befc40ca8708712f34e5f815162addb170f7f3 + languageName: node + linkType: hard + +"@stoplight/types@npm:^12.0.0, @stoplight/types@npm:^12.3.0": + version: 12.5.0 + resolution: "@stoplight/types@npm:12.5.0" + dependencies: + "@types/json-schema": ^7.0.4 + utility-types: ^3.10.0 + checksum: fe4a09df6e1c2f0cdb53f474b180cc7b8184e814e1ac4427d199642f10958335f597060530a908c0e5800ba2569d077afe124a51deaee466255ce942e1e03941 + languageName: node + linkType: hard + "@stoplight/yaml-ast-parser@npm:0.0.48": version: 0.0.48 resolution: "@stoplight/yaml-ast-parser@npm:0.0.48" @@ -3147,16 +3166,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.1.0, acorn@npm:^8.2.4, acorn@npm:^8.4.1": - version: 8.5.0 - resolution: "acorn@npm:8.5.0" - bin: - acorn: bin/acorn - checksum: 2e4c1dbed3da327684863debf31d341bf8882c6893c506653872c00977eee45675feb9129255d6c74c88424d2b20d889ca6de5b39776e5e3cccfc756b3ca1da8 - languageName: node - linkType: hard - -"acorn@npm:^8.7.0": +"acorn@npm:^8.1.0, acorn@npm:^8.2.4, acorn@npm:^8.4.1, acorn@npm:^8.7.0": version: 8.7.0 resolution: "acorn@npm:8.7.0" bin: @@ -8093,7 +8103,7 @@ __metadata: languageName: node linkType: hard -"json-schema@npm:0.4.0, json-schema@npm:^0.4.0": +"json-schema@npm:^0.4.0": version: 0.4.0 resolution: "json-schema@npm:0.4.0" checksum: 66389434c3469e698da0df2e7ac5a3281bcff75e797a5c127db7c5b56270e01ae13d9afa3c03344f76e32e81678337a8c912bdbb75101c62e487dc3778461d72 From 0445b8a0e9c9bee912ad644561b4e948fffd5d02 Mon Sep 17 00:00:00 2001 From: stoplight-bot Date: Fri, 29 Apr 2022 12:12:30 +0000 Subject: [PATCH 07/19] chore(release): 1.12.1 [skip ci] # [@stoplight/spectral-core-v1.12.1](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-core-v1.12.0...@stoplight/spectral-core-v1.12.1) (2022-04-29) ### Bug Fixes * **core:** redeclared rules should always be re-enabled ([#2138](https://github.com/stoplightio/spectral/issues/2138)) ([6def6be](https://github.com/stoplightio/spectral/commit/6def6be4bc3f318c8e93d2e1c0df2ff1b803a178)) --- packages/core/CHANGELOG.md | 7 +++++++ packages/core/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 97fa437b3..8c51d8f84 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@stoplight/spectral-core-v1.12.1](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-core-v1.12.0...@stoplight/spectral-core-v1.12.1) (2022-04-29) + + +### Bug Fixes + +* **core:** redeclared rules should always be re-enabled ([#2138](https://github.com/stoplightio/spectral/issues/2138)) ([6def6be](https://github.com/stoplightio/spectral/commit/6def6be4bc3f318c8e93d2e1c0df2ff1b803a178)) + # [@stoplight/spectral-core-v1.12.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-core-v1.11.1...@stoplight/spectral-core-v1.12.0) (2022-04-18) diff --git a/packages/core/package.json b/packages/core/package.json index b93c49a2e..0e22bd94f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/spectral-core", - "version": "1.12.0", + "version": "1.12.1", "sideEffects": false, "homepage": "https://github.com/stoplightio/spectral", "bugs": "https://github.com/stoplightio/spectral/issues", From 8b5d6b3f91777f2370fd02eecb44f72078e55f01 Mon Sep 17 00:00:00 2001 From: Jonas Lagoni Date: Mon, 2 May 2022 22:42:09 +0200 Subject: [PATCH 08/19] feat(formats): support AsyncAPI 2.4 (#2146) --- docs/getting-started/3-rulesets.md | 1 + packages/formats/src/__tests__/asyncapi.test.ts | 15 ++++++++++++++- packages/formats/src/asyncapi.ts | 5 +++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/getting-started/3-rulesets.md b/docs/getting-started/3-rulesets.md index 38f841ae8..99175e012 100644 --- a/docs/getting-started/3-rulesets.md +++ b/docs/getting-started/3-rulesets.md @@ -80,6 +80,7 @@ Formats are an optional way to specify which API description formats a rule, or - `aas2_1` (AsyncAPI v2.1.0) - `aas2_2` (AsyncAPI v2.2.0) - `aas2_3` (AsyncAPI v2.3.0) +- `aas2_4` (AsyncAPI v2.4.0) - `oas2` (OpenAPI v2.0) - `oas3` (OpenAPI v3.x) - `oas3.0` (OpenAPI v3.0.x) diff --git a/packages/formats/src/__tests__/asyncapi.test.ts b/packages/formats/src/__tests__/asyncapi.test.ts index 01e22c240..9e84dc790 100644 --- a/packages/formats/src/__tests__/asyncapi.test.ts +++ b/packages/formats/src/__tests__/asyncapi.test.ts @@ -1,4 +1,4 @@ -import { aas2, aas2_0, aas2_1, aas2_2, aas2_3 } from '../asyncapi'; +import { aas2, aas2_0, aas2_1, aas2_2, aas2_3, aas2_4 } from '../asyncapi'; describe('AsyncAPI format', () => { describe('AsyncAPI 2.x', () => { @@ -75,4 +75,17 @@ describe('AsyncAPI format', () => { }, ); }); + + describe('AsyncAPI 2.4', () => { + it.each(['2.4.0', '2.4.3'])('recognizes %s version correctly', version => { + expect(aas2_4({ asyncapi: version }, null)).toBe(true); + }); + + it.each(['2', '2.3', '2.0.0', '2.1.0', '2.1.37', '2.2.0', '2.3.0', '2.5.0', '2.5.3'])( + 'does not recognize %s version', + version => { + expect(aas2_4({ asyncapi: version }, null)).toBe(false); + }, + ); + }); }); diff --git a/packages/formats/src/asyncapi.ts b/packages/formats/src/asyncapi.ts index 87312ac19..77b406d8d 100644 --- a/packages/formats/src/asyncapi.ts +++ b/packages/formats/src/asyncapi.ts @@ -8,6 +8,7 @@ const aas2_0Regex = /^2\.0(?:\.[0-9]*)?$/; const aas2_1Regex = /^2\.1(?:\.[0-9]*)?$/; const aas2_2Regex = /^2\.2(?:\.[0-9]*)?$/; const aas2_3Regex = /^2\.3(?:\.[0-9]*)?$/; +const aas2_4Regex = /^2\.4(?:\.[0-9]*)?$/; const isAas2 = (document: unknown): document is { asyncapi: string } & Record => isPlainObject(document) && 'asyncapi' in document && aas2Regex.test(String((document as MaybeAAS2).asyncapi)); @@ -34,3 +35,7 @@ aas2_2.displayName = 'AsyncAPI 2.2.x'; export const aas2_3: Format = (document: unknown): boolean => isAas2(document) && aas2_3Regex.test(String((document as MaybeAAS2).asyncapi)); aas2_3.displayName = 'AsyncAPI 2.3.x'; + +export const aas2_4: Format = (document: unknown): boolean => + isAas2(document) && aas2_4Regex.test(String((document as MaybeAAS2).asyncapi)); +aas2_4.displayName = 'AsyncAPI 2.4.x'; From 06c52b49ac74392838496a4e229b91a7b56da8fb Mon Sep 17 00:00:00 2001 From: stoplight-bot Date: Mon, 2 May 2022 20:53:53 +0000 Subject: [PATCH 09/19] chore(release): 1.2.0 [skip ci] # [@stoplight/spectral-formats-v1.2.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-formats-v1.1.0...@stoplight/spectral-formats-v1.2.0) (2022-05-02) ### Features * **formats:** support AsyncAPI 2.4 ([#2146](https://github.com/stoplightio/spectral/issues/2146)) ([8b5d6b3](https://github.com/stoplightio/spectral/commit/8b5d6b3f91777f2370fd02eecb44f72078e55f01)) --- packages/formats/CHANGELOG.md | 7 +++++++ packages/formats/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/formats/CHANGELOG.md b/packages/formats/CHANGELOG.md index f7feee55b..284e53177 100644 --- a/packages/formats/CHANGELOG.md +++ b/packages/formats/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@stoplight/spectral-formats-v1.2.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-formats-v1.1.0...@stoplight/spectral-formats-v1.2.0) (2022-05-02) + + +### Features + +* **formats:** support AsyncAPI 2.4 ([#2146](https://github.com/stoplightio/spectral/issues/2146)) ([8b5d6b3](https://github.com/stoplightio/spectral/commit/8b5d6b3f91777f2370fd02eecb44f72078e55f01)) + # [@stoplight/spectral-formats-v1.1.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-formats-v1.0.2...@stoplight/spectral-formats-v1.1.0) (2022-02-24) ### Features diff --git a/packages/formats/package.json b/packages/formats/package.json index d5e42d7e8..3b0b4fadb 100644 --- a/packages/formats/package.json +++ b/packages/formats/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/spectral-formats", - "version": "1.1.0", + "version": "1.2.0", "sideEffects": false, "homepage": "https://github.com/stoplightio/spectral", "bugs": "https://github.com/stoplightio/spectral/issues", From 7364b2dd602f4f2a9367d8a8653b621b9fef4326 Mon Sep 17 00:00:00 2001 From: Jonas Lagoni Date: Mon, 2 May 2022 22:57:04 +0200 Subject: [PATCH 10/19] feat(rulesets): support AsyncAPI 2.4 (#2146) --- packages/rulesets/package.json | 6 +++--- .../functions/asyncApi2DocumentSchema.ts | 5 ++++- packages/rulesets/src/asyncapi/index.ts | 4 ++-- yarn.lock | 18 +++++++++--------- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/rulesets/package.json b/packages/rulesets/package.json index 140829a88..19917e39f 100644 --- a/packages/rulesets/package.json +++ b/packages/rulesets/package.json @@ -21,14 +21,14 @@ "release": "semantic-release -e semantic-release-monorepo" }, "dependencies": { - "@asyncapi/specs": "^2.13.0", + "@asyncapi/specs": "^2.14.0", "@stoplight/better-ajv-errors": "1.0.1", "@stoplight/json": "^3.17.0", "@stoplight/spectral-core": "^1.8.1", - "@stoplight/spectral-formats": "^1.1.0", + "@stoplight/spectral-formats": "^1.2.0", "@stoplight/spectral-functions": "^1.5.1", "@stoplight/spectral-runtime": "^1.1.1", - "@stoplight/types": "^12.3.0", + "@stoplight/types": "^12.5.0", "@types/json-schema": "^7.0.7", "ajv": "^8.8.2", "ajv-formats": "~2.1.0", diff --git a/packages/rulesets/src/asyncapi/functions/asyncApi2DocumentSchema.ts b/packages/rulesets/src/asyncapi/functions/asyncApi2DocumentSchema.ts index dceae2ed7..c32d1fd12 100644 --- a/packages/rulesets/src/asyncapi/functions/asyncApi2DocumentSchema.ts +++ b/packages/rulesets/src/asyncapi/functions/asyncApi2DocumentSchema.ts @@ -1,6 +1,6 @@ import { createRulesetFunction } from '@stoplight/spectral-core'; import { schema as schemaFn } from '@stoplight/spectral-functions'; -import { aas2_0, aas2_1, aas2_2, aas2_3 } from '@stoplight/spectral-formats'; +import { aas2_0, aas2_1, aas2_2, aas2_3, aas2_4 } from '@stoplight/spectral-formats'; import type { ErrorObject } from 'ajv'; import type { IFunctionResult, Format } from '@stoplight/spectral-core'; @@ -10,6 +10,7 @@ import * as asyncAPI2_0_0Schema from '@asyncapi/specs/schemas/2.0.0.json'; import * as asyncAPI2_1_0Schema from '@asyncapi/specs/schemas/2.1.0.json'; import * as asyncAPI2_2_0Schema from '@asyncapi/specs/schemas/2.2.0.json'; import * as asyncAPI2_3_0Schema from '@asyncapi/specs/schemas/2.3.0.json'; +import * as asyncAPI2_4_0Schema from '@asyncapi/specs/schemas/2.4.0.json'; function shouldIgnoreError(error: ErrorObject): boolean { return ( @@ -81,6 +82,8 @@ function getSchema(formats: Set): Record | void { return asyncAPI2_2_0Schema; case formats.has(aas2_3): return asyncAPI2_3_0Schema; + case formats.has(aas2_4): + return asyncAPI2_4_0Schema; default: return; } diff --git a/packages/rulesets/src/asyncapi/index.ts b/packages/rulesets/src/asyncapi/index.ts index 434240a9a..c36d3ed13 100644 --- a/packages/rulesets/src/asyncapi/index.ts +++ b/packages/rulesets/src/asyncapi/index.ts @@ -1,4 +1,4 @@ -import { aas2_0, aas2_1, aas2_2, aas2_3 } from '@stoplight/spectral-formats'; +import { aas2_0, aas2_1, aas2_2, aas2_3, aas2_4 } from '@stoplight/spectral-formats'; import { truthy, pattern, @@ -14,7 +14,7 @@ import asyncApi2PayloadValidation from './functions/asyncApi2PayloadValidation'; export default { documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md', - formats: [aas2_0, aas2_1, aas2_2, aas2_3], + formats: [aas2_0, aas2_1, aas2_2, aas2_3, aas2_4], rules: { 'asyncapi-channel-no-empty-parameter': { description: 'Channel path must not have empty parameter substitution pattern.', diff --git a/yarn.lock b/yarn.lock index c222a3281..87c74c58d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,10 +17,10 @@ __metadata: languageName: node linkType: hard -"@asyncapi/specs@npm:^2.13.0": - version: 2.13.0 - resolution: "@asyncapi/specs@npm:2.13.0" - checksum: 94355c96ac2562bfd9118a3e33dd36359196d070684da952f6b0f800b588b426d012fbf96f85b7341ec74f401e3487934b92e43f48e086fa956eab29b90ab694 +"@asyncapi/specs@npm:^2.14.0": + version: 2.14.0 + resolution: "@asyncapi/specs@npm:2.14.0" + checksum: 066c23c493df54c44c319433bdcf8482a3acd584e32c0073e6a9f5b167d61bde23a252621be2b28bbaf1466636f6cafaab570795de403f0c671358784d4b12ed languageName: node linkType: hard @@ -2411,7 +2411,7 @@ __metadata: languageName: unknown linkType: soft -"@stoplight/spectral-formats@*, @stoplight/spectral-formats@>=1, @stoplight/spectral-formats@^1.0.0, @stoplight/spectral-formats@^1.1.0, @stoplight/spectral-formats@workspace:packages/formats": +"@stoplight/spectral-formats@*, @stoplight/spectral-formats@>=1, @stoplight/spectral-formats@^1.0.0, @stoplight/spectral-formats@^1.2.0, @stoplight/spectral-formats@workspace:packages/formats": version: 0.0.0-use.local resolution: "@stoplight/spectral-formats@workspace:packages/formats" dependencies: @@ -2523,17 +2523,17 @@ __metadata: version: 0.0.0-use.local resolution: "@stoplight/spectral-rulesets@workspace:packages/rulesets" dependencies: - "@asyncapi/specs": ^2.13.0 + "@asyncapi/specs": ^2.14.0 "@stoplight/better-ajv-errors": 1.0.1 "@stoplight/json": ^3.17.0 "@stoplight/path": ^1.3.2 "@stoplight/spectral-core": ^1.8.1 - "@stoplight/spectral-formats": ^1.1.0 + "@stoplight/spectral-formats": ^1.2.0 "@stoplight/spectral-functions": ^1.5.1 "@stoplight/spectral-parsers": "*" "@stoplight/spectral-ref-resolver": "*" "@stoplight/spectral-runtime": ^1.1.1 - "@stoplight/types": ^12.3.0 + "@stoplight/types": ^12.5.0 "@types/json-schema": ^7.0.7 ajv: ^8.8.2 ajv-formats: ~2.1.0 @@ -2578,7 +2578,7 @@ __metadata: languageName: node linkType: hard -"@stoplight/types@npm:^12.0.0, @stoplight/types@npm:^12.3.0": +"@stoplight/types@npm:^12.0.0, @stoplight/types@npm:^12.3.0, @stoplight/types@npm:^12.5.0": version: 12.5.0 resolution: "@stoplight/types@npm:12.5.0" dependencies: From e6a90f23bf50d55c544fbc87a3ddf9c75393f44f Mon Sep 17 00:00:00 2001 From: stoplight-bot Date: Tue, 3 May 2022 12:13:40 +0000 Subject: [PATCH 11/19] chore(release): 1.8.0 [skip ci] # [@stoplight/spectral-rulesets-v1.8.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-rulesets-v1.7.0...@stoplight/spectral-rulesets-v1.8.0) (2022-05-03) ### Features * **rulesets:** support AsyncAPI 2.4 ([#2146](https://github.com/stoplightio/spectral/issues/2146)) ([7364b2d](https://github.com/stoplightio/spectral/commit/7364b2dd602f4f2a9367d8a8653b621b9fef4326)) --- packages/rulesets/CHANGELOG.md | 7 +++++++ packages/rulesets/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/rulesets/CHANGELOG.md b/packages/rulesets/CHANGELOG.md index 3e8762c8d..978ccc37f 100644 --- a/packages/rulesets/CHANGELOG.md +++ b/packages/rulesets/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@stoplight/spectral-rulesets-v1.8.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-rulesets-v1.7.0...@stoplight/spectral-rulesets-v1.8.0) (2022-05-03) + + +### Features + +* **rulesets:** support AsyncAPI 2.4 ([#2146](https://github.com/stoplightio/spectral/issues/2146)) ([7364b2d](https://github.com/stoplightio/spectral/commit/7364b2dd602f4f2a9367d8a8653b621b9fef4326)) + # [@stoplight/spectral-rulesets-v1.7.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-rulesets-v1.6.0...@stoplight/spectral-rulesets-v1.7.0) (2022-03-24) diff --git a/packages/rulesets/package.json b/packages/rulesets/package.json index 19917e39f..0b15a3d2e 100644 --- a/packages/rulesets/package.json +++ b/packages/rulesets/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/spectral-rulesets", - "version": "1.7.0", + "version": "1.8.0", "homepage": "https://github.com/stoplightio/spectral", "bugs": "https://github.com/stoplightio/spectral/issues", "author": "Stoplight ", From 84d48cf5e02780f0cbb9ae9074c03a618c2bc462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Wed, 11 May 2022 19:33:06 +0200 Subject: [PATCH 12/19] feat(cli): sort linting results alphabetically (#2147) --- .../invalid-stoplight-info-document.json | 5 ++++ .../cli/src/services/__tests__/linter.test.ts | 25 ++++++++++++++++++- .../src/services/linter/utils/listFiles.ts | 4 ++- 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 packages/cli/src/services/__tests__/__fixtures__/invalid-stoplight-info-document.json diff --git a/packages/cli/src/services/__tests__/__fixtures__/invalid-stoplight-info-document.json b/packages/cli/src/services/__tests__/__fixtures__/invalid-stoplight-info-document.json new file mode 100644 index 000000000..1a71c21b7 --- /dev/null +++ b/packages/cli/src/services/__tests__/__fixtures__/invalid-stoplight-info-document.json @@ -0,0 +1,5 @@ +{ + "info": { + "title": "no stoplight :(" + } +} diff --git a/packages/cli/src/services/__tests__/linter.test.ts b/packages/cli/src/services/__tests__/linter.test.ts index 84ed44522..edcda750e 100644 --- a/packages/cli/src/services/__tests__/linter.test.ts +++ b/packages/cli/src/services/__tests__/linter.test.ts @@ -118,8 +118,8 @@ describe('Linter service', () => { it('given a list of files is provided, outputs issues for each file', () => { const documents = [ + join(__dirname, `./__fixtures__/invalid-stoplight-info-document.json`), join(__dirname, `./__fixtures__/missing-stoplight-info-document.json`), - join(__dirname, `./__fixtures__/missing-stoplight-info-document-copy.json`), ]; return expect(run(['lint', ...documents].join(' '))).resolves.toEqual([ @@ -142,6 +142,29 @@ describe('Linter service', () => { ]); }); + it('sorts linting results in an alphabetical order', () => { + const documents = [ + join(__dirname, `./__fixtures__/missing-stoplight-info-document.json`), + join(__dirname, `./__fixtures__/openapi-3.0-valid.yaml`), + join(__dirname, `./__fixtures__/invalid-stoplight-info-document.json`), + ]; + + return expect(run(['lint', ...documents].join(' '))).resolves.toEqual([ + expect.objectContaining({ + code: 'info-matches-stoplight', + source: join(__dirname, `./__fixtures__/invalid-stoplight-info-document.json`), + }), + expect.objectContaining({ + code: 'info-matches-stoplight', + source: join(__dirname, `./__fixtures__/missing-stoplight-info-document.json`), + }), + expect.objectContaining({ + code: 'info-matches-stoplight', + source: join(__dirname, `./__fixtures__/openapi-3.0-valid.yaml`), + }), + ]); + }); + describe('when glob is provided', () => { const documents = join(__dirname, `./__fixtures__/missing-stoplight-info*.json`); diff --git a/packages/cli/src/services/linter/utils/listFiles.ts b/packages/cli/src/services/linter/utils/listFiles.ts index 2edd01730..8049b2462 100644 --- a/packages/cli/src/services/linter/utils/listFiles.ts +++ b/packages/cli/src/services/linter/utils/listFiles.ts @@ -10,6 +10,8 @@ async function match(pattern: fg.Pattern | fg.Pattern[]): Promise { return (await fg(pattern, GLOB_OPTIONS)).map(normalize); } +const compareString = (a: string, b: string): number => a.localeCompare(b); + export async function listFiles(patterns: string[], ignoreUnmatchedGlobs: boolean): Promise<[string[], string[]]> { const { files, urls } = patterns.reduce<{ files: string[]; @@ -49,5 +51,5 @@ export async function listFiles(patterns: string[], ignoreUnmatchedGlobs: boolea ); } - return [[...urls, ...filesFound], fileSearchWithoutResult]; // let's normalize OS paths produced by fast-glob to have consistent paths across all platforms + return [[...urls, ...filesFound].sort(compareString), fileSearchWithoutResult]; // let's normalize OS paths produced by fast-glob to have consistent paths across all platforms } From b37d1c28031c6941f39e46cca5d5318afc12fee5 Mon Sep 17 00:00:00 2001 From: stoplight-bot Date: Thu, 12 May 2022 12:13:34 +0000 Subject: [PATCH 13/19] chore(release): 6.4.0 [skip ci] # [@stoplight/spectral-cli-v6.4.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-cli-v6.3.0...@stoplight/spectral-cli-v6.4.0) (2022-05-12) ### Features * **cli:** sort linting results alphabetically ([#2147](https://github.com/stoplightio/spectral/issues/2147)) ([84d48cf](https://github.com/stoplightio/spectral/commit/84d48cf5e02780f0cbb9ae9074c03a618c2bc462)) --- packages/cli/CHANGELOG.md | 7 +++++++ packages/cli/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 2f06311b1..4a498cd60 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@stoplight/spectral-cli-v6.4.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-cli-v6.3.0...@stoplight/spectral-cli-v6.4.0) (2022-05-12) + + +### Features + +* **cli:** sort linting results alphabetically ([#2147](https://github.com/stoplightio/spectral/issues/2147)) ([84d48cf](https://github.com/stoplightio/spectral/commit/84d48cf5e02780f0cbb9ae9074c03a618c2bc462)) + # [@stoplight/spectral-cli-v6.3.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-cli-v6.2.1...@stoplight/spectral-cli-v6.3.0) (2022-03-03) diff --git a/packages/cli/package.json b/packages/cli/package.json index c3b86ebb8..74821dd38 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/spectral-cli", - "version": "6.3.0", + "version": "6.4.0", "homepage": "https://github.com/stoplightio/spectral", "bugs": "https://github.com/stoplightio/spectral/issues", "author": "Stoplight ", From 4d5ebebb65cb8f6c44faa5b629311f5b25dd6bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Wed, 18 May 2022 19:39:49 +0200 Subject: [PATCH 14/19] fix(core): bump nimma from 0.2.0 to 0.2.1 (#2157) --- packages/core/package.json | 6 +++--- yarn.lock | 37 +++++++++++++++++++++++++------------ 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 0e22bd94f..1213f888e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -38,13 +38,13 @@ }, "dependencies": { "@stoplight/better-ajv-errors": "1.0.1", - "@stoplight/json": "~3.17.1", + "@stoplight/json": "~3.18.1", "@stoplight/lifecycle": "2.3.2", "@stoplight/path": "1.3.2", "@stoplight/spectral-parsers": "^1.0.0", "@stoplight/spectral-ref-resolver": "^1.0.0", "@stoplight/spectral-runtime": "^1.0.0", - "@stoplight/types": "13.0.0", + "@stoplight/types": "13.1.0", "@types/json-schema": "^7.0.7", "ajv": "^8.6.0", "ajv-errors": "~3.0.0", @@ -54,7 +54,7 @@ "lodash": "~4.17.21", "lodash.topath": "^4.5.2", "minimatch": "3.0.4", - "nimma": "0.2.0", + "nimma": "0.2.1", "pony-cause": "^1.0.0", "simple-eval": "1.0.0", "tslib": "^2.3.0" diff --git a/yarn.lock b/yarn.lock index 87c74c58d..9ad33328b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2298,7 +2298,7 @@ __metadata: languageName: node linkType: hard -"@stoplight/json@npm:3.17.0, @stoplight/json@npm:^3.17.0, @stoplight/json@npm:~3.17.0, @stoplight/json@npm:~3.17.1": +"@stoplight/json@npm:3.17.0, @stoplight/json@npm:~3.17.0, @stoplight/json@npm:~3.17.1": version: 3.17.0 resolution: "@stoplight/json@npm:3.17.0" dependencies: @@ -2311,6 +2311,19 @@ __metadata: languageName: node linkType: hard +"@stoplight/json@npm:^3.17.0, @stoplight/json@npm:~3.18.1": + version: 3.18.1 + resolution: "@stoplight/json@npm:3.18.1" + dependencies: + "@stoplight/ordered-object-literal": ^1.0.2 + "@stoplight/types": ^13.0.0 + jsonc-parser: ~2.2.1 + lodash: ^4.17.21 + safe-stable-stringify: ^1.1 + checksum: 9df3e06421541b6d7f0fbec3f928e67f0d46ef8d4cf79986345164551baef727580e85659c6b9658be578eecd253a6eb23dbe04bccf074d251e6b17a5fddf0a8 + languageName: node + linkType: hard + "@stoplight/lifecycle@npm:2.3.2": version: 2.3.2 resolution: "@stoplight/lifecycle@npm:2.3.2" @@ -2381,7 +2394,7 @@ __metadata: resolution: "@stoplight/spectral-core@workspace:packages/core" dependencies: "@stoplight/better-ajv-errors": 1.0.1 - "@stoplight/json": ~3.17.1 + "@stoplight/json": ~3.18.1 "@stoplight/lifecycle": 2.3.2 "@stoplight/path": 1.3.2 "@stoplight/spectral-formats": "*" @@ -2389,7 +2402,7 @@ __metadata: "@stoplight/spectral-parsers": "*" "@stoplight/spectral-ref-resolver": ^1.0.0 "@stoplight/spectral-runtime": ^1.0.0 - "@stoplight/types": 13.0.0 + "@stoplight/types": 13.1.0 "@stoplight/yaml": ^4.2.2 "@types/json-schema": ^7.0.7 "@types/minimatch": ^3.0.5 @@ -2402,7 +2415,7 @@ __metadata: lodash: ~4.17.21 lodash.topath: ^4.5.2 minimatch: 3.0.4 - nimma: 0.2.0 + nimma: 0.2.1 nock: ^13.1.0 pony-cause: ^1.0.0 simple-eval: 1.0.0 @@ -2568,13 +2581,13 @@ __metadata: languageName: node linkType: hard -"@stoplight/types@npm:13.0.0": - version: 13.0.0 - resolution: "@stoplight/types@npm:13.0.0" +"@stoplight/types@npm:13.1.0, @stoplight/types@npm:^13.0.0": + version: 13.1.0 + resolution: "@stoplight/types@npm:13.1.0" dependencies: "@types/json-schema": ^7.0.4 utility-types: ^3.10.0 - checksum: 40a3995fe4f654b9761908eb41b4369d22241220e96aa0f3ace4541c160d995ddd52dee32fa574a87d7ba3f573befc40ca8708712f34e5f815162addb170f7f3 + checksum: 4caf5374e541a38f905d2e83c81beea8250990fe458fcc299d0acc75bde60d8db17ace2c98afa15ab12014b6f0b536dc8d1d22bc9ce498c60493b9267edab72f languageName: node linkType: hard @@ -9347,9 +9360,9 @@ __metadata: languageName: node linkType: hard -"nimma@npm:0.2.0": - version: 0.2.0 - resolution: "nimma@npm:0.2.0" +"nimma@npm:0.2.1": + version: 0.2.1 + resolution: "nimma@npm:0.2.1" dependencies: "@jsep-plugin/regex": ^1.0.1 "@jsep-plugin/ternary": ^1.0.2 @@ -9362,7 +9375,7 @@ __metadata: optional: true lodash.topath: optional: true - checksum: ed404ab3aca227a51e924e2b8d7d9ae75695c6c3fbdbaffee9990b42b5491f3c240ef3cdbbaffd1b9c3564bf062c936cb8b921de6b21ce8aa22733a00597fd2d + checksum: 5cba1ef3509aca12e33c34fce66cc10ceba23c989b900c26cce39e18685b7036f801924d5a892069c274f875e1616bfdcf4a36fc25400e154f9df252cfc0eb2f languageName: node linkType: hard From daae46c4bea0eb0d3eddc812113c3311ec482485 Mon Sep 17 00:00:00 2001 From: stoplight-bot Date: Wed, 18 May 2022 17:49:21 +0000 Subject: [PATCH 15/19] chore(release): 1.12.2 [skip ci] # [@stoplight/spectral-core-v1.12.2](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-core-v1.12.1...@stoplight/spectral-core-v1.12.2) (2022-05-18) ### Bug Fixes * **core:** bump nimma from 0.2.0 to 0.2.1 ([#2157](https://github.com/stoplightio/spectral/issues/2157)) ([4d5ebeb](https://github.com/stoplightio/spectral/commit/4d5ebebb65cb8f6c44faa5b629311f5b25dd6bfe)) --- packages/core/CHANGELOG.md | 7 +++++++ packages/core/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 8c51d8f84..566350b39 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@stoplight/spectral-core-v1.12.2](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-core-v1.12.1...@stoplight/spectral-core-v1.12.2) (2022-05-18) + + +### Bug Fixes + +* **core:** bump nimma from 0.2.0 to 0.2.1 ([#2157](https://github.com/stoplightio/spectral/issues/2157)) ([4d5ebeb](https://github.com/stoplightio/spectral/commit/4d5ebebb65cb8f6c44faa5b629311f5b25dd6bfe)) + # [@stoplight/spectral-core-v1.12.1](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-core-v1.12.0...@stoplight/spectral-core-v1.12.1) (2022-04-29) diff --git a/packages/core/package.json b/packages/core/package.json index 1213f888e..b02c2a68b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/spectral-core", - "version": "1.12.1", + "version": "1.12.2", "sideEffects": false, "homepage": "https://github.com/stoplightio/spectral", "bugs": "https://github.com/stoplightio/spectral/issues", From 09a0dfa19b0715a08b54ed29eafc26b48a32093d Mon Sep 17 00:00:00 2001 From: Matatjahu Date: Tue, 22 Mar 2022 13:13:08 +0100 Subject: [PATCH 16/19] feat(rulesets): add rules for validation of tag names uniqueness --- docs/reference/asyncapi-rules.md | 22 +++ .../asyncapi-tags-uniqueness.test.ts | 140 ++++++++++++++++++ .../__tests__/asyncApi2UniquenessTags.test.ts | 79 ++++++++++ .../functions/asyncApi2DocumentSchema.ts | 2 +- .../functions/asyncApi2UniquenessTags.ts | 33 +++++ packages/rulesets/src/asyncapi/index.ts | 35 +++++ 6 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 packages/rulesets/src/asyncapi/__tests__/asyncapi-tags-uniqueness.test.ts create mode 100644 packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2UniquenessTags.test.ts create mode 100644 packages/rulesets/src/asyncapi/functions/asyncApi2UniquenessTags.ts diff --git a/docs/reference/asyncapi-rules.md b/docs/reference/asyncapi-rules.md index 4e488bbe2..43666cff4 100644 --- a/docs/reference/asyncapi-rules.md +++ b/docs/reference/asyncapi-rules.md @@ -341,6 +341,28 @@ tags: **Recommended:** No +### asyncapi-tags-uniqueness + +Tags have duplicate names (identifiers). + +**Recommended:** Yes + +**Bad Example** + +```yaml +tags: + - name: "Badger" + - name: "Badger" +``` + +**Good Example** + +```yaml +tags: + - name: "Aardvark" + - name: "Badger" +``` + ### asyncapi-tags AsyncAPI object should have non-empty `tags` array. diff --git a/packages/rulesets/src/asyncapi/__tests__/asyncapi-tags-uniqueness.test.ts b/packages/rulesets/src/asyncapi/__tests__/asyncapi-tags-uniqueness.test.ts new file mode 100644 index 000000000..6f9e451d1 --- /dev/null +++ b/packages/rulesets/src/asyncapi/__tests__/asyncapi-tags-uniqueness.test.ts @@ -0,0 +1,140 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import testRule from './__helpers__/tester'; + +testRule('asyncapi-tags-uniqueness', [ + { + name: 'valid case', + document: { + asyncapi: '2.0.0', + tags: [{ name: 'one' }, { name: 'two' }], + }, + errors: [], + }, + + { + name: 'tags has duplicated names (root)', + document: { + asyncapi: '2.0.0', + tags: [{ name: 'one' }, { name: 'one' }], + }, + errors: [ + { + message: 'Tags contains duplicate tag names: one.', + path: ['tags'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'tags has duplicated names (operation)', + document: { + asyncapi: '2.0.0', + channels: { + someChannel: { + publish: { + tags: [{ name: 'one' }, { name: 'one' }], + }, + subscribe: { + tags: [{ name: 'one' }, { name: 'one' }], + }, + }, + }, + }, + errors: [ + { + message: 'Tags contains duplicate tag names: one.', + path: ['channels', 'someChannel', 'publish', 'tags'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Tags contains duplicate tag names: one.', + path: ['channels', 'someChannel', 'subscribe', 'tags'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'tags has duplicated names (operation trait)', + document: { + asyncapi: '2.0.0', + channels: { + someChannel: { + publish: { + traits: [ + { + tags: [{ name: 'one' }, { name: 'one' }], + }, + ], + }, + subscribe: { + traits: [ + { + tags: [{ name: 'one' }, { name: 'one' }], + }, + ], + }, + }, + }, + }, + errors: [ + { + message: 'Tags contains duplicate tag names: one.', + path: ['channels', 'someChannel', 'publish', 'traits', '0', 'tags'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Tags contains duplicate tag names: one.', + path: ['channels', 'someChannel', 'subscribe', 'traits', '0', 'tags'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'tags has duplicated names (message)', + document: { + asyncapi: '2.0.0', + components: { + messages: { + someMessage: { + tags: [{ name: 'one' }, { name: 'one' }], + }, + }, + }, + }, + errors: [ + { + message: 'Tags contains duplicate tag names: one.', + path: ['components', 'messages', 'someMessage', 'tags'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'tags has duplicated names (message trait)', + document: { + asyncapi: '2.0.0', + components: { + messages: { + someMessage: { + traits: [ + { + tags: [{ name: 'one' }, { name: 'one' }], + }, + ], + }, + }, + }, + }, + errors: [ + { + message: 'Tags contains duplicate tag names: one.', + path: ['components', 'messages', 'someMessage', 'traits', '0', 'tags'], + severity: DiagnosticSeverity.Error, + }, + ], + }, +]); diff --git a/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2UniquenessTags.test.ts b/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2UniquenessTags.test.ts new file mode 100644 index 000000000..cacf21a19 --- /dev/null +++ b/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2UniquenessTags.test.ts @@ -0,0 +1,79 @@ +import asyncApi2UniquenessTags from '../asyncApi2UniquenessTags'; + +function runValidation(targetVal: Array<{ name: string }>) { + return asyncApi2UniquenessTags(targetVal, null, { path: ['tags'], documentInventory: {} } as any); +} + +describe('asyncApi2UniquenessTags', () => { + test('should skip empty tags', () => { + const results = runValidation([]); + expect(results).toEqual([]); + }); + + test('should skip valid tags', () => { + const tags = [ + { + name: 'one', + }, + { + name: 'two', + }, + { + name: 'three', + }, + ]; + + const results = runValidation(tags); + expect(results).toEqual([]); + }); + + test('should check 1 duplicate tags', () => { + const tags = [ + { + name: 'one', + }, + { + name: 'two', + }, + { + name: 'one', + }, + ]; + + const results = runValidation(tags); + expect(results).toEqual([ + { + message: 'Tags contains duplicate tag names: one.', + path: ['tags'], + }, + ]); + }); + + test('should check 2 duplicate tags', () => { + const tags = [ + { + name: 'one', + }, + { + name: 'two', + }, + { + name: 'three', + }, + { + name: 'one', + }, + { + name: 'two', + }, + ]; + + const results = runValidation(tags); + expect(results).toEqual([ + { + message: 'Tags contains duplicate tag names: one, two.', + path: ['tags'], + }, + ]); + }); +}); diff --git a/packages/rulesets/src/asyncapi/functions/asyncApi2DocumentSchema.ts b/packages/rulesets/src/asyncapi/functions/asyncApi2DocumentSchema.ts index c32d1fd12..4ac8381d0 100644 --- a/packages/rulesets/src/asyncapi/functions/asyncApi2DocumentSchema.ts +++ b/packages/rulesets/src/asyncapi/functions/asyncApi2DocumentSchema.ts @@ -94,7 +94,7 @@ export default createRulesetFunction( input: null, options: null, }, - function oasDocumentSchema(targetVal, _, context) { + function asyncApi2DocumentSchema(targetVal, _, context) { const formats = context.document.formats; if (formats === null || formats === void 0) return; diff --git a/packages/rulesets/src/asyncapi/functions/asyncApi2UniquenessTags.ts b/packages/rulesets/src/asyncapi/functions/asyncApi2UniquenessTags.ts new file mode 100644 index 000000000..a7d6da5f4 --- /dev/null +++ b/packages/rulesets/src/asyncapi/functions/asyncApi2UniquenessTags.ts @@ -0,0 +1,33 @@ +import { createRulesetFunction } from '@stoplight/spectral-core'; + +import type { IFunctionResult } from '@stoplight/spectral-core'; + +function getDuplicateTagNames(tags: { name: string }[]) { + const tagNames = tags.map(item => item.name); + return tagNames.reduce((acc, item, idx, arr) => { + if (arr.indexOf(item) !== idx && acc.indexOf(item) < 0) { + acc.push(item); + } + return acc; + }, [] as string[]); +} + +export default createRulesetFunction, null>( + { + input: null, + options: null, + }, + function asyncApi2UniquenessTags(targetVal, _, ctx) { + if (!targetVal || targetVal.length === 0) return []; + + const duplicatedTags = getDuplicateTagNames(targetVal); + if (!duplicatedTags || duplicatedTags.length === 0) return []; + + return [ + { + message: `Tags contains duplicate tag names: ${duplicatedTags.join(', ')}.`, + path: ctx.path, + }, + ] as IFunctionResult[]; + }, +); diff --git a/packages/rulesets/src/asyncapi/index.ts b/packages/rulesets/src/asyncapi/index.ts index c36d3ed13..4b9075e9e 100644 --- a/packages/rulesets/src/asyncapi/index.ts +++ b/packages/rulesets/src/asyncapi/index.ts @@ -11,6 +11,7 @@ import { import asyncApi2DocumentSchema from './functions/asyncApi2DocumentSchema'; import asyncApi2SchemaValidation from './functions/asyncApi2SchemaValidation'; import asyncApi2PayloadValidation from './functions/asyncApi2PayloadValidation'; +import asyncApi2UniquenessTags from './functions/asyncApi2UniquenessTags'; export default { documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md', @@ -358,6 +359,40 @@ export default { }, }, }, + 'asyncapi-tags-uniqueness': { + description: 'Each tags must have a unique names.', + message: '{{error}}', + severity: 'error', + recommended: true, + type: 'validation', + given: [ + // root + '$.tags', + // operations + '$.channels.*.[publish,subscribe].tags', + '$.components.channels.*.[publish,subscribe].tags', + // operation traits + '$.channels.*.[publish,subscribe].traits.*.tags', + '$.components.channels.*.[publish,subscribe].traits.*.tags', + '$.components.operationTraits.*.tags', + // messages + '$.channels.*.[publish,subscribe].message.tags', + '$.channels.*.[publish,subscribe].message.oneOf.*.tags', + '$.components.channels.*.[publish,subscribe].message.tags', + '$.components.channels.*.[publish,subscribe].message.oneOf.*.tags', + '$.components.messages.*.tags', + // message traits + '$.channels.*.[publish,subscribe].message.traits.*.tags', + '$.channels.*.[publish,subscribe].message.oneOf.*.traits.*.tags', + '$.components.channels.*.[publish,subscribe].message.traits.*.tags', + '$.components.channels.*.[publish,subscribe].message.oneOf.*.traits.*.tags', + '$.components.messages.*.traits.*.tags', + '$.components.messageTraits.*.tags', + ], + then: { + function: asyncApi2UniquenessTags, + }, + }, 'asyncapi-tags': { description: 'AsyncAPI object must have non-empty "tags" array.', recommended: true, From 7f36799aef759ec58e6dd22a8b5dc3e1759d805a Mon Sep 17 00:00:00 2001 From: Matatjahu Date: Thu, 7 Apr 2022 14:37:07 +0200 Subject: [PATCH 17/19] feat(rulesets): propagate review suggestions --- .../asyncapi-tags-uniqueness.test.ts | 48 ++++++++++----- .../functions/asyncApi2UniquenessTags.ts | 33 ---------- packages/rulesets/src/asyncapi/index.ts | 4 +- .../__tests__/openapi-tags-uniqueness.test.ts | 48 +++++++++++++++ packages/rulesets/src/oas/index.ts | 12 ++++ .../__tests__/uniquenessTags.test.ts} | 25 +++++--- .../rulesets/src/shared/functions/index.ts | 1 + .../src/shared/functions/uniquenessTags.ts | 61 +++++++++++++++++++ 8 files changed, 176 insertions(+), 56 deletions(-) delete mode 100644 packages/rulesets/src/asyncapi/functions/asyncApi2UniquenessTags.ts create mode 100644 packages/rulesets/src/oas/__tests__/openapi-tags-uniqueness.test.ts rename packages/rulesets/src/{asyncapi/functions/__tests__/asyncApi2UniquenessTags.test.ts => shared/functions/__tests__/uniquenessTags.test.ts} (62%) create mode 100644 packages/rulesets/src/shared/functions/index.ts create mode 100644 packages/rulesets/src/shared/functions/uniquenessTags.ts diff --git a/packages/rulesets/src/asyncapi/__tests__/asyncapi-tags-uniqueness.test.ts b/packages/rulesets/src/asyncapi/__tests__/asyncapi-tags-uniqueness.test.ts index 6f9e451d1..2c2f77ea7 100644 --- a/packages/rulesets/src/asyncapi/__tests__/asyncapi-tags-uniqueness.test.ts +++ b/packages/rulesets/src/asyncapi/__tests__/asyncapi-tags-uniqueness.test.ts @@ -19,8 +19,8 @@ testRule('asyncapi-tags-uniqueness', [ }, errors: [ { - message: 'Tags contains duplicate tag names: one.', - path: ['tags'], + message: '"tags" object contains duplicate tag name "one".', + path: ['tags', '1', 'name'], severity: DiagnosticSeverity.Error, }, ], @@ -43,13 +43,13 @@ testRule('asyncapi-tags-uniqueness', [ }, errors: [ { - message: 'Tags contains duplicate tag names: one.', - path: ['channels', 'someChannel', 'publish', 'tags'], + message: '"tags" object contains duplicate tag name "one".', + path: ['channels', 'someChannel', 'publish', 'tags', '1', 'name'], severity: DiagnosticSeverity.Error, }, { - message: 'Tags contains duplicate tag names: one.', - path: ['channels', 'someChannel', 'subscribe', 'tags'], + message: '"tags" object contains duplicate tag name "one".', + path: ['channels', 'someChannel', 'subscribe', 'tags', '1', 'name'], severity: DiagnosticSeverity.Error, }, ], @@ -80,13 +80,13 @@ testRule('asyncapi-tags-uniqueness', [ }, errors: [ { - message: 'Tags contains duplicate tag names: one.', - path: ['channels', 'someChannel', 'publish', 'traits', '0', 'tags'], + message: '"tags" object contains duplicate tag name "one".', + path: ['channels', 'someChannel', 'publish', 'traits', '0', 'tags', '1', 'name'], severity: DiagnosticSeverity.Error, }, { - message: 'Tags contains duplicate tag names: one.', - path: ['channels', 'someChannel', 'subscribe', 'traits', '0', 'tags'], + message: '"tags" object contains duplicate tag name "one".', + path: ['channels', 'someChannel', 'subscribe', 'traits', '0', 'tags', '1', 'name'], severity: DiagnosticSeverity.Error, }, ], @@ -106,8 +106,8 @@ testRule('asyncapi-tags-uniqueness', [ }, errors: [ { - message: 'Tags contains duplicate tag names: one.', - path: ['components', 'messages', 'someMessage', 'tags'], + message: '"tags" object contains duplicate tag name "one".', + path: ['components', 'messages', 'someMessage', 'tags', '1', 'name'], severity: DiagnosticSeverity.Error, }, ], @@ -131,8 +131,28 @@ testRule('asyncapi-tags-uniqueness', [ }, errors: [ { - message: 'Tags contains duplicate tag names: one.', - path: ['components', 'messages', 'someMessage', 'traits', '0', 'tags'], + message: '"tags" object contains duplicate tag name "one".', + path: ['components', 'messages', 'someMessage', 'traits', '0', 'tags', '1', 'name'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'tags has duplicated more that two times this same name', + document: { + asyncapi: '2.0.0', + tags: [{ name: 'one' }, { name: 'one' }, { name: 'two' }, { name: 'one' }], + }, + errors: [ + { + message: '"tags" object contains duplicate tag name "one".', + path: ['tags', '1', 'name'], + severity: DiagnosticSeverity.Error, + }, + { + message: '"tags" object contains duplicate tag name "one".', + path: ['tags', '3', 'name'], severity: DiagnosticSeverity.Error, }, ], diff --git a/packages/rulesets/src/asyncapi/functions/asyncApi2UniquenessTags.ts b/packages/rulesets/src/asyncapi/functions/asyncApi2UniquenessTags.ts deleted file mode 100644 index a7d6da5f4..000000000 --- a/packages/rulesets/src/asyncapi/functions/asyncApi2UniquenessTags.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { createRulesetFunction } from '@stoplight/spectral-core'; - -import type { IFunctionResult } from '@stoplight/spectral-core'; - -function getDuplicateTagNames(tags: { name: string }[]) { - const tagNames = tags.map(item => item.name); - return tagNames.reduce((acc, item, idx, arr) => { - if (arr.indexOf(item) !== idx && acc.indexOf(item) < 0) { - acc.push(item); - } - return acc; - }, [] as string[]); -} - -export default createRulesetFunction, null>( - { - input: null, - options: null, - }, - function asyncApi2UniquenessTags(targetVal, _, ctx) { - if (!targetVal || targetVal.length === 0) return []; - - const duplicatedTags = getDuplicateTagNames(targetVal); - if (!duplicatedTags || duplicatedTags.length === 0) return []; - - return [ - { - message: `Tags contains duplicate tag names: ${duplicatedTags.join(', ')}.`, - path: ctx.path, - }, - ] as IFunctionResult[]; - }, -); diff --git a/packages/rulesets/src/asyncapi/index.ts b/packages/rulesets/src/asyncapi/index.ts index 4b9075e9e..0a74000d3 100644 --- a/packages/rulesets/src/asyncapi/index.ts +++ b/packages/rulesets/src/asyncapi/index.ts @@ -11,7 +11,7 @@ import { import asyncApi2DocumentSchema from './functions/asyncApi2DocumentSchema'; import asyncApi2SchemaValidation from './functions/asyncApi2SchemaValidation'; import asyncApi2PayloadValidation from './functions/asyncApi2PayloadValidation'; -import asyncApi2UniquenessTags from './functions/asyncApi2UniquenessTags'; +import { uniquenessTags } from '../shared/functions'; export default { documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md', @@ -390,7 +390,7 @@ export default { '$.components.messageTraits.*.tags', ], then: { - function: asyncApi2UniquenessTags, + function: uniquenessTags, }, }, 'asyncapi-tags': { diff --git a/packages/rulesets/src/oas/__tests__/openapi-tags-uniqueness.test.ts b/packages/rulesets/src/oas/__tests__/openapi-tags-uniqueness.test.ts new file mode 100644 index 000000000..61e735edb --- /dev/null +++ b/packages/rulesets/src/oas/__tests__/openapi-tags-uniqueness.test.ts @@ -0,0 +1,48 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import testRule from './__helpers__/tester'; + +testRule('openapi-tags-uniqueness', [ + { + name: 'valid case', + document: { + swagger: '2.0', + tags: [{ name: 'one' }, { name: 'two' }], + }, + errors: [], + }, + + { + name: 'tags has duplicated names', + document: { + swagger: '2.0', + tags: [{ name: 'one' }, { name: 'one' }], + }, + errors: [ + { + message: '"tags" object contains duplicate tag name "one".', + path: ['tags', '1', 'name'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'tags has duplicated more that two times this same name', + document: { + swagger: '2.0', + tags: [{ name: 'one' }, { name: 'one' }, { name: 'two' }, { name: 'one' }], + }, + errors: [ + { + message: '"tags" object contains duplicate tag name "one".', + path: ['tags', '1', 'name'], + severity: DiagnosticSeverity.Error, + }, + { + message: '"tags" object contains duplicate tag name "one".', + path: ['tags', '3', 'name'], + severity: DiagnosticSeverity.Error, + }, + ], + }, +]); diff --git a/packages/rulesets/src/oas/index.ts b/packages/rulesets/src/oas/index.ts index 8d7e817ff..23e65a683 100644 --- a/packages/rulesets/src/oas/index.ts +++ b/packages/rulesets/src/oas/index.ts @@ -25,6 +25,7 @@ import { oasSchema, oasDiscriminator, } from './functions'; +import { uniquenessTags } from '../shared/functions'; export { ruleset as default }; @@ -212,6 +213,17 @@ const ruleset = { }, }, }, + 'openapi-tags-uniqueness': { + description: 'Each tags must have a unique names.', + message: '{{error}}', + severity: 'error', + recommended: true, + type: 'validation', + given: '$.tags', + then: { + function: uniquenessTags, + }, + }, 'openapi-tags': { description: 'OpenAPI object must have non-empty "tags" array.', recommended: false, diff --git a/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2UniquenessTags.test.ts b/packages/rulesets/src/shared/functions/__tests__/uniquenessTags.test.ts similarity index 62% rename from packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2UniquenessTags.test.ts rename to packages/rulesets/src/shared/functions/__tests__/uniquenessTags.test.ts index cacf21a19..5abb9a849 100644 --- a/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2UniquenessTags.test.ts +++ b/packages/rulesets/src/shared/functions/__tests__/uniquenessTags.test.ts @@ -1,10 +1,10 @@ -import asyncApi2UniquenessTags from '../asyncApi2UniquenessTags'; +import uniquenessTags from '../uniquenessTags'; function runValidation(targetVal: Array<{ name: string }>) { - return asyncApi2UniquenessTags(targetVal, null, { path: ['tags'], documentInventory: {} } as any); + return uniquenessTags(targetVal, null, { path: ['tags'], documentInventory: {} } as any); } -describe('asyncApi2UniquenessTags', () => { +describe('uniquenessTags', () => { test('should skip empty tags', () => { const results = runValidation([]); expect(results).toEqual([]); @@ -43,8 +43,8 @@ describe('asyncApi2UniquenessTags', () => { const results = runValidation(tags); expect(results).toEqual([ { - message: 'Tags contains duplicate tag names: one.', - path: ['tags'], + message: '"tags" object contains duplicate tag name "one".', + path: ['tags', 2, 'name'], }, ]); }); @@ -66,13 +66,24 @@ describe('asyncApi2UniquenessTags', () => { { name: 'two', }, + { + name: 'two', + }, ]; const results = runValidation(tags); expect(results).toEqual([ { - message: 'Tags contains duplicate tag names: one, two.', - path: ['tags'], + message: '"tags" object contains duplicate tag name "one".', + path: ['tags', 3, 'name'], + }, + { + message: '"tags" object contains duplicate tag name "two".', + path: ['tags', 4, 'name'], + }, + { + message: '"tags" object contains duplicate tag name "two".', + path: ['tags', 5, 'name'], }, ]); }); diff --git a/packages/rulesets/src/shared/functions/index.ts b/packages/rulesets/src/shared/functions/index.ts new file mode 100644 index 000000000..0d2f1ca8a --- /dev/null +++ b/packages/rulesets/src/shared/functions/index.ts @@ -0,0 +1 @@ +export { default as uniquenessTags } from './uniquenessTags'; diff --git a/packages/rulesets/src/shared/functions/uniquenessTags.ts b/packages/rulesets/src/shared/functions/uniquenessTags.ts new file mode 100644 index 000000000..e478bfb17 --- /dev/null +++ b/packages/rulesets/src/shared/functions/uniquenessTags.ts @@ -0,0 +1,61 @@ +import { createRulesetFunction } from '@stoplight/spectral-core'; + +import type { IFunctionResult } from '@stoplight/spectral-core'; + +type Tags = Array<{ name: string }>; + +function getDuplicateTagNames(tags: Tags): string[] { + const tagNames = tags.map(item => item.name); + return tagNames.reduce((acc, item, idx, arr) => { + if (arr.indexOf(item) !== idx && acc.indexOf(item) < 0) { + acc.push(item); + } + return acc; + }, [] as string[]); +} + +export default createRulesetFunction( + { + input: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + }, + }, + }, + }, + options: null, + }, + function uniquenessTags(targetVal, _, ctx) { + const duplicatedTags = getDuplicateTagNames(targetVal); + if (duplicatedTags.length === 0) return []; + + const results: IFunctionResult[] = []; + + duplicatedTags.map(duplicatedTag => { + let checkedFirst = false; + const duplicatedTags: number[] = []; + targetVal.forEach((tag, index) => { + if (tag.name === duplicatedTag) { + if (!checkedFirst) { + checkedFirst = true; + return; + } + duplicatedTags.push(index); + } + }); + + results.push( + ...duplicatedTags.map(duplicatedIndex => ({ + message: `"tags" object contains duplicate tag name "${duplicatedTag}".`, + path: [...ctx.path, duplicatedIndex, 'name'], + })), + ); + }); + + return results; + }, +); From 7765b2231b8a563a5998ad1c82bdf03769e8a201 Mon Sep 17 00:00:00 2001 From: Matatjahu Date: Thu, 7 Apr 2022 14:43:24 +0200 Subject: [PATCH 18/19] feat(rulesets): add docs for new openapi rule --- docs/reference/openapi-rules.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/reference/openapi-rules.md b/docs/reference/openapi-rules.md index 2e4e17f3b..69e1884c8 100644 --- a/docs/reference/openapi-rules.md +++ b/docs/reference/openapi-rules.md @@ -250,6 +250,28 @@ tags: - name: "Badger" ``` +### asyncapi-tags-uniqueness + +OpenAPI object should have non-duplicated tag names (identifiers). + +**Recommended:** Yes + +**Bad Example** + +```yaml +tags: + - name: "Badger" + - name: "Badger" +``` + +**Good Example** + +```yaml +tags: + - name: "Aardvark" + - name: "Badger" +``` + ### openapi-tags OpenAPI object should have non-empty `tags` array. From f070f4e6708b2bd9a50614497fcfb65759f58492 Mon Sep 17 00:00:00 2001 From: Matatjahu Date: Thu, 19 May 2022 12:36:11 +0200 Subject: [PATCH 19/19] feat(rulesets): apply review suggestions --- docs/reference/asyncapi-rules.md | 2 +- docs/reference/openapi-rules.md | 4 +- packages/rulesets/src/asyncapi/index.ts | 2 +- packages/rulesets/src/oas/index.ts | 2 +- .../src/shared/functions/uniquenessTags.ts | 44 +++++++------------ 5 files changed, 21 insertions(+), 33 deletions(-) diff --git a/docs/reference/asyncapi-rules.md b/docs/reference/asyncapi-rules.md index 43666cff4..fa11723df 100644 --- a/docs/reference/asyncapi-rules.md +++ b/docs/reference/asyncapi-rules.md @@ -343,7 +343,7 @@ tags: ### asyncapi-tags-uniqueness -Tags have duplicate names (identifiers). +Tags must not have duplicate names (identifiers). **Recommended:** Yes diff --git a/docs/reference/openapi-rules.md b/docs/reference/openapi-rules.md index 69e1884c8..3abcbcb47 100644 --- a/docs/reference/openapi-rules.md +++ b/docs/reference/openapi-rules.md @@ -250,9 +250,9 @@ tags: - name: "Badger" ``` -### asyncapi-tags-uniqueness +### openapi-tags-uniqueness -OpenAPI object should have non-duplicated tag names (identifiers). +OpenAPI object must not have duplicated tag names (identifiers). **Recommended:** Yes diff --git a/packages/rulesets/src/asyncapi/index.ts b/packages/rulesets/src/asyncapi/index.ts index 0a74000d3..660c1bb38 100644 --- a/packages/rulesets/src/asyncapi/index.ts +++ b/packages/rulesets/src/asyncapi/index.ts @@ -360,7 +360,7 @@ export default { }, }, 'asyncapi-tags-uniqueness': { - description: 'Each tags must have a unique names.', + description: 'Each tag must have a unique name.', message: '{{error}}', severity: 'error', recommended: true, diff --git a/packages/rulesets/src/oas/index.ts b/packages/rulesets/src/oas/index.ts index 23e65a683..9f5c21e74 100644 --- a/packages/rulesets/src/oas/index.ts +++ b/packages/rulesets/src/oas/index.ts @@ -214,7 +214,7 @@ const ruleset = { }, }, 'openapi-tags-uniqueness': { - description: 'Each tags must have a unique names.', + description: 'Each tag must have a unique name.', message: '{{error}}', severity: 'error', recommended: true, diff --git a/packages/rulesets/src/shared/functions/uniquenessTags.ts b/packages/rulesets/src/shared/functions/uniquenessTags.ts index e478bfb17..ba711db2c 100644 --- a/packages/rulesets/src/shared/functions/uniquenessTags.ts +++ b/packages/rulesets/src/shared/functions/uniquenessTags.ts @@ -4,14 +4,15 @@ import type { IFunctionResult } from '@stoplight/spectral-core'; type Tags = Array<{ name: string }>; -function getDuplicateTagNames(tags: Tags): string[] { - const tagNames = tags.map(item => item.name); - return tagNames.reduce((acc, item, idx, arr) => { - if (arr.indexOf(item) !== idx && acc.indexOf(item) < 0) { - acc.push(item); - } - return acc; - }, [] as string[]); +function getDuplicateTagsIndexes(tags: Tags): number[] { + return tags + .map(item => item.name) + .reduce((acc, item, i, arr) => { + if (arr.indexOf(item) !== i) { + acc.push(i); + } + return acc; + }, []); } export default createRulesetFunction( @@ -30,31 +31,18 @@ export default createRulesetFunction( options: null, }, function uniquenessTags(targetVal, _, ctx) { - const duplicatedTags = getDuplicateTagNames(targetVal); + const duplicatedTags = getDuplicateTagsIndexes(targetVal); if (duplicatedTags.length === 0) return []; const results: IFunctionResult[] = []; - duplicatedTags.map(duplicatedTag => { - let checkedFirst = false; - const duplicatedTags: number[] = []; - targetVal.forEach((tag, index) => { - if (tag.name === duplicatedTag) { - if (!checkedFirst) { - checkedFirst = true; - return; - } - duplicatedTags.push(index); - } + for (const duplicatedIndex of duplicatedTags) { + const duplicatedTag = targetVal[duplicatedIndex].name; + results.push({ + message: `"tags" object contains duplicate tag name "${duplicatedTag}".`, + path: [...ctx.path, duplicatedIndex, 'name'], }); - - results.push( - ...duplicatedTags.map(duplicatedIndex => ({ - message: `"tags" object contains duplicate tag name "${duplicatedTag}".`, - path: [...ctx.path, duplicatedIndex, 'name'], - })), - ); - }); + } return results; },