diff --git a/.eslintignore b/.eslintignore index 21e1ae991..22b0d1dad 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ **/__fixtures__/** /test-harness/**/*.yaml /packages/*/dist +/packages/*/CHANGELOG.md diff --git a/__karma__/perf_hooks.js b/__karma__/perf_hooks.js new file mode 100644 index 000000000..e69de29bb diff --git a/__karma__/process.js b/__karma__/process.js index f237ddf58..98594c821 100644 --- a/__karma__/process.js +++ b/__karma__/process.js @@ -1 +1 @@ -export default undefined; +export const on = Function(); diff --git a/docs/guides/4-custom-rulesets.md b/docs/guides/4-custom-rulesets.md index 2e03a2402..527a850a7 100644 --- a/docs/guides/4-custom-rulesets.md +++ b/docs/guides/4-custom-rulesets.md @@ -402,38 +402,53 @@ Rulesets can then reference aliases in the [given](#given) keyword, either in fu Previously Spectral supported exceptions, which were limited in their ability to target particular rules on specific files or parts of files, or changing parts of a rule. Overrides is the much more powerful version of exceptions, with the ability to customize ruleset usage for different files and projects without having to duplicate any rules. -Overrides can be used to: +Overrides can be used to apply rulesets on: -- Override rulesets to apply on particular files/folders `files: ['schemas/**/*.draft7.json']` -- Override rulesets to apply on particular JSONPath's `files: ['**#/components/schemas/Item']` -- Override rulesets to apply on particular formats `formats: [jsonSchemaDraft7]` +- Particular formats `formats: [jsonSchemaDraft7]` +- Particular files/folders `files: ['schemas/**/*.draft7.json']` +- Particular elements of files `files: ['**#/components/schemas/Item']` - Override particular rules **Example** +```yaml +overrides: + formats: + - json-schema-draft7 + files: + - schemas/**/*.draft7.json + rules: + valid-number-validation: + given: + - $..exclusiveMinimum + - $..exclusiveMaximum + then: + function: schema + functionOptions: + type: number +``` + +To apply an override to particular elements of files, combine a glob for a filepath +with a [JSON Pointer](https://datatracker.ietf.org/doc/html/rfc6901) after the anchor, i.e.: + ```yaml overrides: - files: - - schemas/**/*.draft7.json - formats: - - json-schema-draft7 + - "legacy/**/*.oas.json#/paths" rules: - valid-number-validation: - given: - - $..exclusiveMinimum - - $..exclusiveMaximum - then: - function: schema - functionOptions: - type: number + some-inherited-rule: "off" ``` -One can also combine a glob for a filepath with a JSONPath after the anchor, i.e.: +JSON Pointers have a different syntax than JSON Paths used in the `given` component of a rule. +In JSON Pointers, path components are prefixed with a "/" and then concatenated to form the pointer. +Since "/" has a special meaning in JSON pointer, it must be encoded as "~1" when it appears in a component, and "~" must be encoded as "~0". + +You can test JSON Pointer expressions in the [JSON Query online evaluator](https://www.jsonquerytool.com/) by choosing "JSONPointer" as the Transform. ```yaml overrides: - files: - - "legacy/**/*.oas.json#/paths" + - "legacy/**/*.oas.json#/paths/~1Pets~1{petId}/get/parameters/0" rules: some-inherited-rule: "off" ``` diff --git a/karma.conf.ts b/karma.conf.ts index 45917c494..2581b4bb6 100644 --- a/karma.conf.ts +++ b/karma.conf.ts @@ -17,7 +17,7 @@ module.exports = (config: Config): void => { files: ['./__karma__/jest.ts', 'packages/*/src/**/*.ts'], // list of files / patterns to exclude - exclude: ['packages/cli/**', '**/*.jest.test.ts'], + exclude: ['packages/cli/**', 'packages/ruleset-bundler/src/plugins/commonjs.ts', '**/*.jest.test.ts'], // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor @@ -41,6 +41,7 @@ module.exports = (config: Config): void => { 'node-fetch': require.resolve('./__karma__/fetch'), fs: require.resolve('./__karma__/fs'), process: require.resolve('./__karma__/process'), + perf_hooks: require.resolve('./__karma__/perf_hooks'), fsevents: require.resolve('./__karma__/fsevents'), }, }, diff --git a/package.json b/package.json index 69f0b9dc0..601bd616e 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 packages/*/CHANGELOG.md docs/**/*.md README.md", + "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", "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", @@ -38,7 +38,7 @@ "test.karma": "karma start", "prepare": "husky install", "prerelease": "patch-package", - "release": "yarn prerelease && HUSKY=0 yarn workspaces foreach run release" + "release": "yarn prerelease && yarn workspaces foreach run release" }, "workspaces": { "packages": [ @@ -100,7 +100,7 @@ "node-powershell": "^4.0.0", "patch-package": "^6.4.7", "prettier": "^2.4.1", - "semantic-release": "^19.0.2", + "semantic-release": "^18.0.1", "semantic-release-monorepo": "^7.0.5", "ts-jest": "^27.0.7", "ts-node": "^10.4.0", @@ -110,9 +110,6 @@ "*.{ts,js}": [ "eslint --fix --cache --cache-location .cache/.eslintcache" ], - "packages/*/CHANGELOG.md": [ - "prettier --write" - ], "docs/**/*.md": [ "prettier --ignore-path .eslintignore --write" ], diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 7985baace..3297825b8 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,3 +1,9 @@ +# [@stoplight/spectral-core-v1.10.2](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-core-v1.10.1...@stoplight/spectral-core-v1.10.2) (2022-02-24) + +### Bug Fixes + +- bump nimma from 0.1.7 to 0.1.8 ([#2058](https://github.com/stoplightio/spectral/issues/2058)) ([fb756f2](https://github.com/stoplightio/spectral/commit/fb756f2e582e533d79c1ac3ed5cef2e8f8b1b299)) + # [@stoplight/spectral-core-v1.10.1](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-core-v1.10.0...@stoplight/spectral-core-v1.10.1) (2022-02-14) ### Bug Fixes diff --git a/packages/core/package.json b/packages/core/package.json index c8a2649f9..50e1195e3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/spectral-core", - "version": "1.10.1", + "version": "1.10.2", "main": "dist/index.js", "types": "dist/index.d.ts", "sideEffects": false, diff --git a/packages/formats/CHANGELOG.md b/packages/formats/CHANGELOG.md index d546c4a8e..f7feee55b 100644 --- a/packages/formats/CHANGELOG.md +++ b/packages/formats/CHANGELOG.md @@ -1,3 +1,9 @@ +# [@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 + +- support 2.1.0, 2.2.0, 2.3.0 AsyncAPI versions ([#2067](https://github.com/stoplightio/spectral/issues/2067)) ([b0b008d](https://github.com/stoplightio/spectral/commit/b0b008d65794df177dbfe7d9589c90d541c2794d)) + # [@stoplight/spectral-formats-v1.0.2](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-formats-v1.0.1...@stoplight/spectral-formats-v1.0.2) (2021-12-30) ### Bug Fixes diff --git a/packages/formats/package.json b/packages/formats/package.json index 01035af03..d5e42d7e8 100644 --- a/packages/formats/package.json +++ b/packages/formats/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/spectral-formats", - "version": "1.0.2", + "version": "1.1.0", "sideEffects": false, "homepage": "https://github.com/stoplightio/spectral", "bugs": "https://github.com/stoplightio/spectral/issues", diff --git a/packages/formats/src/__tests__/asyncapi.test.ts b/packages/formats/src/__tests__/asyncapi.test.ts index 9bd0de0cb..01e22c240 100644 --- a/packages/formats/src/__tests__/asyncapi.test.ts +++ b/packages/formats/src/__tests__/asyncapi.test.ts @@ -1,10 +1,13 @@ -import { asyncApi2 } from '../asyncapi'; +import { aas2, aas2_0, aas2_1, aas2_2, aas2_3 } from '../asyncapi'; -describe('AsyncApi format', () => { - describe('AsyncApi 2.{minor}.{patch}', () => { - it.each([['2.0.17'], ['2.9.0'], ['2.9.3']])('recognizes %s version correctly', (version: string) => { - expect(asyncApi2({ asyncapi: version }, null)).toBe(true); - }); +describe('AsyncAPI format', () => { + describe('AsyncAPI 2.x', () => { + it.each(['2.0.0', '2.1.0', '2.2.0', '2.3.0', '2.0.17', '2.1.37', '2.9.0', '2.9.3'])( + 'recognizes %s version correctly', + version => { + expect(aas2({ asyncapi: version }, null)).toBe(true); + }, + ); const testCases = [ { asyncapi: '3.0' }, @@ -15,6 +18,7 @@ describe('AsyncApi format', () => { { asyncapi: '2.0.01' }, { asyncapi: '1.0' }, { asyncapi: 2 }, + { asyncapi: null }, { openapi: '4.0' }, { openapi: '2.0' }, { openapi: null }, @@ -25,7 +29,50 @@ describe('AsyncApi format', () => { ]; it.each(testCases)('does not recognize invalid document %o', document => { - expect(asyncApi2(document, null)).toBe(false); + expect(aas2(document, null)).toBe(false); + }); + }); + + describe('AsyncAPI 2.0', () => { + it.each(['2.0.0', '2.0.3'])('recognizes %s version correctly', version => { + expect(aas2_0({ asyncapi: version }, null)).toBe(true); + }); + + it.each(['2', '2.0', '2.1.0', '2.1.3'])('does not recognize %s version', version => { + expect(aas2_0({ asyncapi: version }, null)).toBe(false); + }); + }); + + describe('AsyncAPI 2.1', () => { + it.each(['2.1.0', '2.1.37'])('recognizes %s version correctly', version => { + expect(aas2_1({ asyncapi: version }, null)).toBe(true); + }); + + it.each(['2', '2.1', '2.0.0', '2.2.0', '2.2.3'])('does not recognize %s version', version => { + expect(aas2_1({ asyncapi: version }, null)).toBe(false); + }); + }); + + describe('AsyncAPI 2.2', () => { + it.each(['2.2.0', '2.2.3'])('recognizes %s version correctly', version => { + expect(aas2_2({ asyncapi: version }, null)).toBe(true); + }); + + it.each(['2', '2.2', '2.0.0', '2.1.0', '2.1.37', '2.3.0', '2.3.3'])('does not recognize %s version', version => { + expect(aas2_2({ asyncapi: version }, null)).toBe(false); }); }); + + describe('AsyncAPI 2.3', () => { + it.each(['2.3.0', '2.3.3'])('recognizes %s version correctly', version => { + expect(aas2_3({ asyncapi: version }, null)).toBe(true); + }); + + it.each(['2', '2.3', '2.0.0', '2.1.0', '2.1.37', '2.2.0', '2.4.0', '2.4.3'])( + 'does not recognize %s version', + version => { + expect(aas2_3({ asyncapi: version }, null)).toBe(false); + }, + ); + }); }); diff --git a/packages/formats/src/asyncapi.ts b/packages/formats/src/asyncapi.ts index 8a1a6dde3..87312ac19 100644 --- a/packages/formats/src/asyncapi.ts +++ b/packages/formats/src/asyncapi.ts @@ -1,24 +1,36 @@ import type { Format } from '@stoplight/spectral-core'; import { isPlainObject } from '@stoplight/json'; -type MaybeAsyncApi2 = Partial<{ asyncapi: unknown }>; +type MaybeAAS2 = { asyncapi: unknown } & Record; -const bearsAStringPropertyNamed = (document: unknown, propertyName: string): boolean => { - return isPlainObject(document) && propertyName in document && typeof document[propertyName] === 'string'; -}; +const aas2Regex = /^2\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/; +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 version2Regex = /^2\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/; +const isAas2 = (document: unknown): document is { asyncapi: string } & Record => + isPlainObject(document) && 'asyncapi' in document && aas2Regex.test(String((document as MaybeAAS2).asyncapi)); -export const asyncApi2: Format = document => { - if (!bearsAStringPropertyNamed(document, 'asyncapi')) { - return false; - } +export const aas2: Format = isAas2; +aas2.displayName = 'AsyncAPI 2.x'; - const version = String((document as MaybeAsyncApi2).asyncapi); +// for backward compatibility +export const asyncApi2 = aas2; +export const asyncapi2 = aas2; - return version2Regex.test(version); -}; +export const aas2_0: Format = (document: unknown): boolean => + isAas2(document) && aas2_0Regex.test(String((document as MaybeAAS2).asyncapi)); +aas2_0.displayName = 'AsyncAPI 2.0.x'; -asyncApi2.displayName = 'AsyncAPI 2.x'; +export const aas2_1: Format = (document: unknown): boolean => + isAas2(document) && aas2_1Regex.test(String((document as MaybeAAS2).asyncapi)); +aas2_1.displayName = 'AsyncAPI 2.1.x'; -export { asyncApi2 as asyncapi2 }; +export const aas2_2: Format = (document: unknown): boolean => + isAas2(document) && aas2_2Regex.test(String((document as MaybeAAS2).asyncapi)); +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'; diff --git a/packages/functions/CHANGELOG.md b/packages/functions/CHANGELOG.md index 2c270784b..28776a150 100644 --- a/packages/functions/CHANGELOG.md +++ b/packages/functions/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@stoplight/spectral-functions-v1.5.2](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-functions-v1.5.1...@stoplight/spectral-functions-v1.5.2) (2022-02-28) + + +### Bug Fixes + +* **functions:** __importDefault undefined ([609ecb1](https://github.com/stoplightio/spectral/commit/609ecb1b23f354459f96687f27f911e915cb6ab3)) + # [@stoplight/spectral-functions-v1.5.1](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-functions-v1.5.0...@stoplight/spectral-functions-v1.5.1) (2021-12-29) ### Bug Fixes diff --git a/packages/functions/package.json b/packages/functions/package.json index 6205c5071..1aa07290d 100644 --- a/packages/functions/package.json +++ b/packages/functions/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/spectral-functions", - "version": "1.5.1", + "version": "1.5.2", "sideEffects": false, "homepage": "https://github.com/stoplightio/spectral", "bugs": "https://github.com/stoplightio/spectral/issues", diff --git a/packages/functions/src/index.ts b/packages/functions/src/index.ts index 04652dd19..8f5e2e1c8 100644 --- a/packages/functions/src/index.ts +++ b/packages/functions/src/index.ts @@ -1,15 +1,41 @@ -export { default as alphabetical, Options as AlphabeticalOptions } from './alphabetical'; -export { default as casing, Options as CasingOptions } from './casing'; -export { default as defined } from './defined'; -export { default as enumeration, Options as EnumerationOptions } from './enumeration'; -export { default as falsy } from './falsy'; -export { default as length, Options as LengthOptions } from './length'; -export { default as pattern, Options as PatternOptions } from './pattern'; -export { default as schema, Options as SchemaOptions } from './schema'; -export { default as truthy } from './truthy'; -export { default as undefined } from './undefined'; -export { +import { default as alphabetical, Options as AlphabeticalOptions } from './alphabetical'; +import { default as casing, Options as CasingOptions } from './casing'; +import { default as defined } from './defined'; +import { default as enumeration, Options as EnumerationOptions } from './enumeration'; +import { default as falsy } from './falsy'; +import { default as length, Options as LengthOptions } from './length'; +import { default as pattern, Options as PatternOptions } from './pattern'; +import { default as schema, Options as SchemaOptions } from './schema'; +import { default as truthy } from './truthy'; +import { default as undefined } from './undefined'; +import { default as unreferencedReusableObject, Options as UnreferencedReusableObjectOptions, } from './unreferencedReusableObject'; -export { default as xor, Options as XorOptions } from './xor'; +import { default as xor, Options as XorOptions } from './xor'; + +export { + alphabetical, + casing, + defined, + enumeration, + falsy, + length, + pattern, + schema, + truthy, + undefined, + unreferencedReusableObject, + xor, +}; + +export type { + AlphabeticalOptions, + CasingOptions, + EnumerationOptions, + LengthOptions, + PatternOptions, + SchemaOptions, + UnreferencedReusableObjectOptions, + XorOptions, +}; diff --git a/packages/ruleset-bundler/CHANGELOG.md b/packages/ruleset-bundler/CHANGELOG.md index 642c9ec93..08edf9858 100644 --- a/packages/ruleset-bundler/CHANGELOG.md +++ b/packages/ruleset-bundler/CHANGELOG.md @@ -1,3 +1,24 @@ +# [@stoplight/spectral-ruleset-bundler-v1.2.1](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-ruleset-bundler-v1.2.0...@stoplight/spectral-ruleset-bundler-v1.2.1) (2022-02-28) + + +### Bug Fixes + +* **ruleset-bundler:** __importDefault undefined ([874a80e](https://github.com/stoplightio/spectral/commit/874a80e9d8e36d96bfbb467e340aab337227bfa7)) + +# [@stoplight/spectral-ruleset-bundler-v1.2.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-ruleset-bundler-v1.1.1...@stoplight/spectral-ruleset-bundler-v1.2.0) (2022-02-25) + + +### Bug Fixes + +* **ruleset-bundler:** builtins plugin should create a new instance for each module ([b06903c](https://github.com/stoplightio/spectral/commit/b06903ce71f556809b06a21ce3a299625b3760e0)) +* **ruleset-bundler:** virtualFs plugin incompatible with commonjs plugin ([a48381b](https://github.com/stoplightio/spectral/commit/a48381bdf86c7c9015dd67daa8bda767ea727376)) + + +### Features + +* **ruleset-bundler:** expose commonjs plugin ([91a4b80](https://github.com/stoplightio/spectral/commit/91a4b807dc1e9b7ed700b6645eff711cfa1d5bef)) +* **ruleset-bundler:** plugins should be easy to override ([0263bf0](https://github.com/stoplightio/spectral/commit/0263bf0234b11d6bb17b7b7feef6ba5716cc8f01)) + # [@stoplight/spectral-ruleset-bundler-v1.1.1](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-ruleset-bundler-v1.1.0...@stoplight/spectral-ruleset-bundler-v1.1.1) (2021-12-30) ### Bug Fixes diff --git a/packages/ruleset-bundler/package.json b/packages/ruleset-bundler/package.json index 0761b7dcf..933d44b5a 100644 --- a/packages/ruleset-bundler/package.json +++ b/packages/ruleset-bundler/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/spectral-ruleset-bundler", - "version": "1.1.1", + "version": "1.2.1", "homepage": "https://github.com/stoplightio/spectral", "bugs": "https://github.com/stoplightio/spectral/issues", "author": "Stoplight ", @@ -38,6 +38,7 @@ "release": "semantic-release -e semantic-release-monorepo" }, "dependencies": { + "@rollup/plugin-commonjs": "^21.0.1", "@stoplight/path": "1.3.2", "@stoplight/spectral-core": ">=1", "@stoplight/spectral-formats": ">=1", @@ -50,7 +51,7 @@ "@stoplight/types": "^12.3.0", "@types/node": "*", "pony-cause": "1.1.1", - "rollup": "~2.60.2", + "rollup": "~2.67.0", "tslib": "^2.3.1", "validate-npm-package-name": "3.0.0" }, diff --git a/packages/ruleset-bundler/src/__tests__/index.jest.test.ts b/packages/ruleset-bundler/src/__tests__/index.jest.test.ts new file mode 100644 index 000000000..9afc25fe0 --- /dev/null +++ b/packages/ruleset-bundler/src/__tests__/index.jest.test.ts @@ -0,0 +1,100 @@ +import { serveAssets } from '@stoplight/spectral-test-utils'; +import { fetch } from '@stoplight/spectral-runtime'; +import * as fs from 'fs'; +import { bundleRuleset } from '../index'; +import { IO } from '../types'; +import { node } from '../presets/node'; +import { browser } from '../presets/browser'; +import { commonjs } from '../plugins/commonjs'; +import { virtualFs } from '../plugins/virtualFs'; +import { runtime } from '../presets/runtime'; + +jest.mock('fs'); + +describe('Ruleset Bundler', () => { + let io: IO; + + beforeEach(() => { + io = { + fs, + fetch, + }; + + serveAssets({ + '/p/.spectral/my-fn.js': `module.exports = function f() { return [] };`, + + '/p/spectral.js': `import myFn from './.spectral/my-fn.js'; + +export default { + rules: { + rule: { + given: '$', + then: { function: myFn }, + } + }, +};`, + }); + }); + + it('given runtime target, should support commonjs', async () => { + const code = await bundleRuleset('/p/spectral.js', { + target: 'runtime', + plugins: [...runtime(io), commonjs()], + }); + + expect(code).toContain(`\tvar myFn = function f() { return [] }; + +\tvar spectral = { +\t rules: { +\t rule: { +\t given: '$', +\t then: { function: myFn }, +\t } +\t }, +\t}; + +\treturn spectral; + +})();`); + }); + + it('given browser target, should support commonjs', async () => { + const code = await bundleRuleset('/p/spectral.js', { + target: 'browser', + plugins: [...browser(io), commonjs()], + }); + + expect(code).toContain(`var myFn = function f() { return [] }; + +var spectral = { + rules: { + rule: { + given: '$', + then: { function: myFn }, + } + }, +}; + +export { spectral as default };`); + }); + + it('given node target, should support commonjs', async () => { + const code = await bundleRuleset('/p/spectral.js', { + target: 'node', + plugins: [...node(io), virtualFs(io), commonjs()], + }); + + expect(code).toContain(`var myFn = function f() { return [] }; + +var spectral = { + rules: { + rule: { + given: '$', + then: { function: myFn }, + } + }, +}; + +export { spectral as default };`); + }); +}); diff --git a/packages/ruleset-bundler/src/index.ts b/packages/ruleset-bundler/src/index.ts index 2228defd9..38acec50f 100644 --- a/packages/ruleset-bundler/src/index.ts +++ b/packages/ruleset-bundler/src/index.ts @@ -1,6 +1,7 @@ import { rollup, Plugin } from 'rollup'; import { isURL } from '@stoplight/path'; import { isPackageImport } from './utils/isPackageImport'; +import { dedupeRollupPlugins } from './utils/dedupeRollupPlugins'; export type BundleOptions = { plugins: Plugin[]; @@ -17,7 +18,7 @@ export async function bundleRuleset( ): Promise { const bundle = await rollup({ input, - plugins, + plugins: dedupeRollupPlugins(plugins), treeshake, watch: false, perf: false, diff --git a/packages/ruleset-bundler/src/plugins/__tests__/builtins.spec.ts b/packages/ruleset-bundler/src/plugins/__tests__/builtins.spec.ts index 8a122f182..44f3fc9ba 100644 --- a/packages/ruleset-bundler/src/plugins/__tests__/builtins.spec.ts +++ b/packages/ruleset-bundler/src/plugins/__tests__/builtins.spec.ts @@ -12,12 +12,23 @@ import { builtins } from '../builtins'; describe('Builtins Plugin', () => { let io: IO; + let randomSpy: jest.SpyInstance; beforeEach(() => { io = { fs, fetch: runtime.fetch, }; + + randomSpy = jest + .spyOn(Math, 'random') + .mockReturnValueOnce(0.8229275205939697) + .mockReturnValueOnce(0.7505242801973444) + .mockReturnValueOnce(0.5647855410879519); + }); + + afterEach(() => { + randomSpy.mockRestore(); }); describe.each(['browser', 'runtime'])('given %s target', target => { @@ -51,21 +62,21 @@ export default { }); expect(code) - .toEqual(`const alphabetical = globalThis[Symbol.for('@stoplight/spectral-functions')]['alphabetical']; -const casing = globalThis[Symbol.for('@stoplight/spectral-functions')]['casing']; -const defined = globalThis[Symbol.for('@stoplight/spectral-functions')]['defined']; -const enumeration = globalThis[Symbol.for('@stoplight/spectral-functions')]['enumeration']; -const falsy = globalThis[Symbol.for('@stoplight/spectral-functions')]['falsy']; -const length = globalThis[Symbol.for('@stoplight/spectral-functions')]['length']; -const pattern = globalThis[Symbol.for('@stoplight/spectral-functions')]['pattern']; -const schema = globalThis[Symbol.for('@stoplight/spectral-functions')]['schema']; -const truthy = globalThis[Symbol.for('@stoplight/spectral-functions')]['truthy']; -const undefined$1 = globalThis[Symbol.for('@stoplight/spectral-functions')]['undefined']; -const unreferencedReusableObject = globalThis[Symbol.for('@stoplight/spectral-functions')]['unreferencedReusableObject']; -const xor = globalThis[Symbol.for('@stoplight/spectral-functions')]['xor']; - -const oas = globalThis[Symbol.for('@stoplight/spectral-rulesets')]['oas']; -const asyncapi = globalThis[Symbol.for('@stoplight/spectral-rulesets')]['asyncapi']; + .toEqual(`const alphabetical = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['alphabetical']; +const casing = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['casing']; +const defined = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['defined']; +const enumeration = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['enumeration']; +const falsy = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['falsy']; +const length = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['length']; +const pattern = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['pattern']; +const schema = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['schema']; +const truthy = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['truthy']; +const undefined$1 = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['undefined']; +const unreferencedReusableObject = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['unreferencedReusableObject']; +const xor = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['xor']; + +const oas = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-rulesets']['oas']; +const asyncapi = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-rulesets']['asyncapi']; var input = { extends: [oas], @@ -87,7 +98,9 @@ var input = { export { input as default }; `); - expect(globalThis[Symbol.for('@stoplight/spectral-functions')]).toStrictEqual(functions); + expect( + globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions'], + ).toStrictEqual(functions); }); it('should support overrides', async () => { @@ -113,30 +126,78 @@ readFile();`, ], }); - expect(code).toEqual(`const fetch = globalThis[Symbol.for('@stoplight/spectral-runtime')]['fetch']; -const DEFAULT_REQUEST_OPTIONS = globalThis[Symbol.for('@stoplight/spectral-runtime')]['DEFAULT_REQUEST_OPTIONS']; -const decodeSegmentFragment = globalThis[Symbol.for('@stoplight/spectral-runtime')]['decodeSegmentFragment']; -const printError = globalThis[Symbol.for('@stoplight/spectral-runtime')]['printError']; -const PrintStyle = globalThis[Symbol.for('@stoplight/spectral-runtime')]['PrintStyle']; -const printPath = globalThis[Symbol.for('@stoplight/spectral-runtime')]['printPath']; -const printValue = globalThis[Symbol.for('@stoplight/spectral-runtime')]['printValue']; -const startsWithProtocol = globalThis[Symbol.for('@stoplight/spectral-runtime')]['startsWithProtocol']; -const isAbsoluteRef = globalThis[Symbol.for('@stoplight/spectral-runtime')]['isAbsoluteRef']; -const traverseObjUntilRef = globalThis[Symbol.for('@stoplight/spectral-runtime')]['traverseObjUntilRef']; -const getEndRef = globalThis[Symbol.for('@stoplight/spectral-runtime')]['getEndRef']; -const safePointerToPath = globalThis[Symbol.for('@stoplight/spectral-runtime')]['safePointerToPath']; -const getClosestJsonPath = globalThis[Symbol.for('@stoplight/spectral-runtime')]['getClosestJsonPath']; -const readFile = globalThis[Symbol.for('@stoplight/spectral-runtime')]['readFile']; -const readParsable = globalThis[Symbol.for('@stoplight/spectral-runtime')]['readParsable']; + expect(code) + .toEqual(`const fetch = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['fetch']; +const DEFAULT_REQUEST_OPTIONS = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['DEFAULT_REQUEST_OPTIONS']; +const decodeSegmentFragment = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['decodeSegmentFragment']; +const printError = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['printError']; +const PrintStyle = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['PrintStyle']; +const printPath = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['printPath']; +const printValue = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['printValue']; +const startsWithProtocol = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['startsWithProtocol']; +const isAbsoluteRef = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['isAbsoluteRef']; +const traverseObjUntilRef = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['traverseObjUntilRef']; +const getEndRef = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['getEndRef']; +const safePointerToPath = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['safePointerToPath']; +const getClosestJsonPath = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['getClosestJsonPath']; +const readFile = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['readFile']; +const readParsable = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['readParsable']; readFile(); `); - expect(globalThis[Symbol.for('@stoplight/spectral-runtime')]).toStrictEqual({ + expect( + globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime'], + ).toStrictEqual({ ...runtime, readFile, }); }); + + it('should isolate each instance', async () => { + serveAssets({ + '/tmp/input.js': `import { readFile } from '@stoplight/spectral-runtime'; + +readFile();`, + }); + + // eslint-disable-next-line @typescript-eslint/no-empty-function + function readFile(): void {} + // eslint-disable-next-line @typescript-eslint/no-empty-function + function readFile2(): void {} + + await bundleRuleset('/tmp/input.js', { + format: 'esm', + target, + plugins: [ + builtins({ + '@stoplight/spectral-runtime': { + readFile, + }, + }), + builtins({ + '@stoplight/spectral-runtime': { + readFile: readFile2, + }, + }), + virtualFs(io), + ], + }); + + expect( + globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime'], + ).toStrictEqual({ + ...runtime, + readFile, + }); + + expect( + globalThis[Symbol.for('@stoplight-spectral/builtins')]['750524']['@stoplight/spectral-runtime'], + ).toStrictEqual({ + ...runtime, + readFile: readFile2, + }); + }); }); describe('given node target', () => { @@ -191,7 +252,9 @@ var input = { export { input as default }; `); - expect(globalThis[Symbol.for('@stoplight/spectral-functions')]).toStrictEqual(functions); + expect( + globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions'], + ).toStrictEqual(functions); }); }); }); diff --git a/packages/ruleset-bundler/src/plugins/builtins.ts b/packages/ruleset-bundler/src/plugins/builtins.ts index 66708f18c..937a831cc 100644 --- a/packages/ruleset-bundler/src/plugins/builtins.ts +++ b/packages/ruleset-bundler/src/plugins/builtins.ts @@ -11,15 +11,21 @@ type Module = 'core' | 'formats' | 'functions' | 'parsers' | 'ref-resolver' | 'r type GlobalModules = Record<`@stoplight/spectral-${Module}`, string>; type Overrides = Record>; +const NAME = '@stoplight-spectral/builtins'; + function registerModule( + instanceId: number, id: keyof GlobalModules, members: Record, overrides: Partial, ): [string, string] { const actualOverrides = overrides[id]; - globalThis[Symbol.for(id)] = actualOverrides ? { ...members, ...actualOverrides } : members; + const instances = (globalThis[Symbol.for(NAME)] ??= {}) as Record>; + const root = (instances[instanceId] ??= {}); + + root[id] = actualOverrides ? { ...members, ...actualOverrides } : members; - const m = `globalThis[Symbol.for('${id}')]`; + const m = `globalThis[Symbol.for('${NAME}')]['${instanceId}']['${id}']`; let code = ''; for (const member of Object.keys(members)) { code += `export const ${member} = ${m}['${member}'];\n`; @@ -29,26 +35,28 @@ function registerModule( } export const builtins = (overrides: Partial = {}): Plugin => { + const instanceId = Math.round(Math.random() * 1_000_000); + const modules = Object.fromEntries([ - registerModule('@stoplight/spectral-core', core, overrides), - registerModule('@stoplight/spectral-formats', formats, overrides), - registerModule('@stoplight/spectral-functions', functions, overrides), - registerModule('@stoplight/spectral-parsers', parsers, overrides), - registerModule('@stoplight/spectral-ref-resolver', refResolver, overrides), - registerModule('@stoplight/spectral-rulesets', rulesets, overrides), - registerModule('@stoplight/spectral-runtime', runtime, overrides), + registerModule(instanceId, '@stoplight/spectral-core', core, overrides), + registerModule(instanceId, '@stoplight/spectral-formats', formats, overrides), + registerModule(instanceId, '@stoplight/spectral-functions', functions, overrides), + registerModule(instanceId, '@stoplight/spectral-parsers', parsers, overrides), + registerModule(instanceId, '@stoplight/spectral-ref-resolver', refResolver, overrides), + registerModule(instanceId, '@stoplight/spectral-rulesets', rulesets, overrides), + registerModule(instanceId, '@stoplight/spectral-runtime', runtime, overrides), ]) as GlobalModules; return { - name: '@stoplight-spectral/builtins', - resolveId(id) { + name: NAME, + resolveId(id): string | null { if (id in modules) { return id; } return null; }, - load(id) { + load(id): string | undefined { if (id in modules) { return modules[id] as string; } diff --git a/packages/ruleset-bundler/src/plugins/commonjs.ts b/packages/ruleset-bundler/src/plugins/commonjs.ts new file mode 100644 index 000000000..f7640bbee --- /dev/null +++ b/packages/ruleset-bundler/src/plugins/commonjs.ts @@ -0,0 +1,3 @@ +import { default as commonjs } from '@rollup/plugin-commonjs'; + +export { commonjs }; diff --git a/packages/ruleset-bundler/src/plugins/virtualFs.ts b/packages/ruleset-bundler/src/plugins/virtualFs.ts index 6ba92dfc5..6c64d851d 100644 --- a/packages/ruleset-bundler/src/plugins/virtualFs.ts +++ b/packages/ruleset-bundler/src/plugins/virtualFs.ts @@ -1,35 +1,48 @@ import { dirname, parse, join, normalize, isAbsolute, isURL } from '@stoplight/path'; -import type { Plugin } from 'rollup'; +import type { Plugin, PluginContext } from 'rollup'; import type { IO } from '../types'; -export const virtualFs = ({ fs }: IO): Plugin => ({ - name: '@stoplight-spectral/virtual-fs', - resolveId(source, importer) { - const { protocol } = parse(source); - - if (protocol === 'http' || protocol === 'https') { - return null; - } - - if (protocol !== 'file' && !/^[./]/.test(source)) { - return null; - } - - if (isAbsolute(source)) { - return normalize(source); - } - - if (importer !== void 0) { - return join(dirname(importer), source); - } - - return source; - }, - load(id) { - if (!isURL(id)) { - return fs.promises.readFile(id, 'utf8'); - } - - return; - }, -}); +export const virtualFs = ({ fs }: IO): Plugin => { + const recognized = new WeakMap(); + + return { + name: '@stoplight-spectral/virtual-fs', + + resolveId(source, importer): string | null { + const { protocol } = parse(source); + + if (protocol === 'http' || protocol === 'https') { + return null; + } + + if (protocol !== 'file' && !/^[./]/.test(source)) { + return null; + } + + let resolvedSource = source; + + if (isAbsolute(source)) { + resolvedSource = normalize(source); + } else if (importer !== void 0) { + resolvedSource = join(dirname(importer), source); + } + + let existingEntries = recognized.get(this); + if (existingEntries === void 0) { + existingEntries = []; + recognized.set(this, existingEntries); + } + + existingEntries.push(resolvedSource); + + return resolvedSource; + }, + load(id): Promise | undefined { + if (!isURL(id) && recognized.get(this)?.includes(id) === true) { + return fs.promises.readFile(id, 'utf8'); + } + + return; + }, + }; +}; diff --git a/packages/ruleset-bundler/src/utils/__tests__/dedupeRollupPlugins.spec.ts b/packages/ruleset-bundler/src/utils/__tests__/dedupeRollupPlugins.spec.ts new file mode 100644 index 000000000..aef577791 --- /dev/null +++ b/packages/ruleset-bundler/src/utils/__tests__/dedupeRollupPlugins.spec.ts @@ -0,0 +1,51 @@ +import type { Plugin } from 'rollup'; + +import { dedupeRollupPlugins } from '../dedupeRollupPlugins'; + +describe('dedupeRollupPlugins util', () => { + it('should keep plugins with different names', () => { + const plugins: Plugin[] = [ + { + name: 'plugin 1', + }, + { + name: 'plugin 2', + }, + { + name: 'plugin 3', + }, + ]; + + expect(dedupeRollupPlugins([...plugins])).toStrictEqual(plugins); + }); + + it('given the same plugin, should replace the first declaration', () => { + const plugins: Plugin[] = [ + { + name: 'plugin 1', + cacheKey: 'key 1', + }, + { + name: 'plugin 2', + }, + { + name: 'plugin 1', + cacheKey: 'key 2', + }, + { + name: 'plugin 1', + cacheKey: 'key 3', + }, + ]; + + expect(dedupeRollupPlugins([...plugins])).toStrictEqual([ + { + name: 'plugin 1', + cacheKey: 'key 3', + }, + { + name: 'plugin 2', + }, + ]); + }); +}); diff --git a/packages/ruleset-bundler/src/utils/dedupeRollupPlugins.ts b/packages/ruleset-bundler/src/utils/dedupeRollupPlugins.ts new file mode 100644 index 000000000..12ba67f6a --- /dev/null +++ b/packages/ruleset-bundler/src/utils/dedupeRollupPlugins.ts @@ -0,0 +1,12 @@ +// this function makes sure we can only have one plugin with the same name +// the last plugin definition has a precedence +import type { Plugin } from 'rollup'; + +export function dedupeRollupPlugins(plugins: Plugin[]): Plugin[] { + const map = new Map(); + for (const plugin of plugins) { + map.set(plugin.name, plugin); + } + + return Array.from(map.values()); +} diff --git a/packages/rulesets/CHANGELOG.md b/packages/rulesets/CHANGELOG.md index 9395cb95b..c03c3798e 100644 --- a/packages/rulesets/CHANGELOG.md +++ b/packages/rulesets/CHANGELOG.md @@ -1,3 +1,30 @@ +# [@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) + + +### Features + +* **rulesets:** validate API security in oas-operation-security-defined ([#2046](https://github.com/stoplightio/spectral/issues/2046)) ([5540250](https://github.com/stoplightio/spectral/commit/5540250035f0df290eb0cb0106606a2918471ec5)) + +# [@stoplight/spectral-rulesets-v1.5.2](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-rulesets-v1.5.1...@stoplight/spectral-rulesets-v1.5.2) (2022-02-28) + + +### Bug Fixes + +* **rulesets:** __importDefault undefined ([fdd647b](https://github.com/stoplightio/spectral/commit/fdd647b36b8d05c264b2320f0c8ea108e587d686)) + +# [@stoplight/spectral-rulesets-v1.5.1](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-rulesets-v1.5.0...@stoplight/spectral-rulesets-v1.5.1) (2022-02-28) + + +### Bug Fixes + +* **rulesets:** __importDefault undefined ([c123bdf](https://github.com/stoplightio/spectral/commit/c123bdf1dfe4d303bf477dc5c211e5b09bb37ed6)) + +# [@stoplight/spectral-rulesets-v1.5.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-rulesets-v1.4.3...@stoplight/spectral-rulesets-v1.5.0) (2022-02-24) + +### Features + +- **rulesets:** support 2.1.0, 2.2.0, 2.3.0 AsyncAPI versions ([#2067](https://github.com/stoplightio/spectral/issues/2067)) ([2f1d7bf](https://github.com/stoplightio/spectral/commit/2f1d7bf31010bc91102d844bf4279a784cad2d67)) + # [@stoplight/spectral-rulesets-v1.4.3](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-rulesets-v1.4.2...@stoplight/spectral-rulesets-v1.4.3) (2022-02-14) ### Bug Fixes diff --git a/packages/rulesets/package.json b/packages/rulesets/package.json index b0225574f..7275e4b2e 100644 --- a/packages/rulesets/package.json +++ b/packages/rulesets/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/spectral-rulesets", - "version": "1.4.3", + "version": "1.6.0", "homepage": "https://github.com/stoplightio/spectral", "bugs": "https://github.com/stoplightio/spectral/issues", "author": "Stoplight ", @@ -21,10 +21,11 @@ "release": "semantic-release -e semantic-release-monorepo" }, "dependencies": { + "@asyncapi/specs": "^2.13.0", "@stoplight/better-ajv-errors": "1.0.1", "@stoplight/json": "^3.17.0", "@stoplight/spectral-core": "^1.8.1", - "@stoplight/spectral-formats": "^1.0.2", + "@stoplight/spectral-formats": "^1.1.0", "@stoplight/spectral-functions": "^1.5.1", "@stoplight/spectral-runtime": "^1.1.1", "@stoplight/types": "^12.3.0", diff --git a/packages/rulesets/src/asyncapi/__tests__/__helpers__/tester.ts b/packages/rulesets/src/asyncapi/__tests__/__helpers__/tester.ts index e23221019..5eaf939aa 100644 --- a/packages/rulesets/src/asyncapi/__tests__/__helpers__/tester.ts +++ b/packages/rulesets/src/asyncapi/__tests__/__helpers__/tester.ts @@ -1,3 +1,3 @@ -import testRule from '../../../__tests__/__helpers__/tester'; +import testRule, { createWithRules } from '../../../__tests__/__helpers__/tester'; -export { testRule as default }; +export { testRule as default, createWithRules }; diff --git a/packages/rulesets/src/asyncapi/__tests__/asyncapi-schema.test.ts b/packages/rulesets/src/asyncapi/__tests__/asyncapi-schema.test.ts index 02b194ed1..0385bfb49 100644 --- a/packages/rulesets/src/asyncapi/__tests__/asyncapi-schema.test.ts +++ b/packages/rulesets/src/asyncapi/__tests__/asyncapi-schema.test.ts @@ -14,7 +14,6 @@ testRule('asyncapi-schema', [ }, errors: [], }, - { name: 'channels property is missing', document: { @@ -24,8 +23,6 @@ testRule('asyncapi-schema', [ version: '1.0', }, }, - errors: [ - { message: 'Object must have required property "channels"', path: [], severity: DiagnosticSeverity.Error }, - ], + errors: [{ message: 'Object must have required property "channels"', severity: DiagnosticSeverity.Error }], }, ]); diff --git a/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2DocumentSchema.test.ts b/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2DocumentSchema.test.ts new file mode 100644 index 000000000..2d8a80c47 --- /dev/null +++ b/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2DocumentSchema.test.ts @@ -0,0 +1,349 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import { Spectral } from '@stoplight/spectral-core'; +import { prepareResults } from '../asyncApi2DocumentSchema'; + +import { ErrorObject } from 'ajv'; +import { createWithRules } from '../../__tests__/__helpers__/tester'; + +describe('asyncApi2DocumentSchema', () => { + let s: Spectral; + + beforeEach(async () => { + s = createWithRules(['asyncapi-schema']); + }); + + describe('given AsyncAPI 2.0.0 document', () => { + test('validate invalid info object', async () => { + expect( + await s.run({ + asyncapi: '2.0.0', + info: { + version: '1.0.1', + description: 'This is a sample server.', + termsOfService: 'http://asyncapi.org/terms/', + }, + channels: { + '/user/signedup': { + subscribe: { + message: { + payload: { + type: 'object', + properties: { + email: { + type: 'string', + format: 'email', + }, + }, + }, + }, + }, + }, + }, + }), + ).toEqual([ + { + code: 'asyncapi-schema', + message: '"info" property must have required property "title"', + path: ['info'], + severity: DiagnosticSeverity.Error, + range: expect.any(Object), + }, + ]); + }); + }); + + describe('given AsyncAPI 2.1.0 document', () => { + test('validate with message examples', async () => { + expect( + await s.run({ + asyncapi: '2.1.0', + info: { + title: 'Signup service example (internal)', + version: '0.1.0', + }, + channels: { + '/user/signedup': { + subscribe: { + message: { + payload: { + type: 'object', + properties: { + email: { + type: 'string', + format: 'email', + }, + }, + }, + examples: [ + { + name: 'Example 1', + summary: 'Example summary for example 1', + payload: { + email: 'bye@foo.bar', + }, + }, + ], + }, + }, + }, + }, + }), + ).toEqual([]); + }); + }); + + describe('given AsyncAPI 2.2.0 document', () => { + test('validate channel with connected server', async () => { + expect( + await s.run({ + asyncapi: '2.2.0', + info: { + title: 'Signup service example (internal)', + version: '0.1.0', + }, + servers: { + kafka: { + url: 'development.gigantic-server.com', + description: 'Development server', + protocol: 'kafka', + protocolVersion: '1.0.0', + }, + }, + channels: { + '/user/signedup': { + servers: [1, 'foobar', 3], + subscribe: { + message: { + payload: { + type: 'object', + properties: { + email: { + type: 'string', + format: 'email', + }, + }, + }, + }, + }, + }, + }, + }), + ).toEqual([ + { + code: 'asyncapi-schema', + message: '"0" property type must be string', + path: ['channels', '/user/signedup', 'servers', '0'], + severity: DiagnosticSeverity.Error, + range: expect.any(Object), + }, + { + code: 'asyncapi-schema', + message: '"2" property type must be string', + path: ['channels', '/user/signedup', 'servers', '2'], + severity: DiagnosticSeverity.Error, + range: expect.any(Object), + }, + ]); + }); + }); + + describe('given AsyncAPI 2.3.0 document', () => { + test('validate reusable server', async () => { + expect( + await s.run({ + asyncapi: '2.3.0', + info: { + title: 'Signup service example (internal)', + version: '0.1.0', + }, + channels: { + '/user/signedup': { + subscribe: { + message: { + payload: { + type: 'object', + properties: { + email: { + type: 'string', + format: 'email', + }, + }, + }, + }, + }, + }, + }, + components: { + servers: { + kafka: { + description: 'Development server', + }, + }, + }, + }), + ).toEqual([ + { + code: 'asyncapi-schema', + message: '"kafka" property must have required property "url"', + path: ['components', 'servers', 'kafka'], + severity: DiagnosticSeverity.Error, + range: expect.any(Object), + }, + ]); + }); + }); + + describe('prepareResults', () => { + test('given oneOf error one of which is required $ref property missing, picks only one error', () => { + const errors: ErrorObject[] = [ + { + keyword: 'type', + instancePath: '/paths/test/post/parameters/0/schema/type', + schemaPath: '#/properties/type/type', + params: { type: 'string' }, + message: 'must be string', + }, + { + keyword: 'required', + instancePath: '/paths/test/post/parameters/0/schema', + schemaPath: '#/definitions/Reference/required', + params: { missingProperty: '$ref' }, + message: "must have required property '$ref'", + }, + { + keyword: 'oneOf', + instancePath: '/paths/test/post/parameters/0/schema', + schemaPath: '#/properties/schema/oneOf', + params: { passingSchemas: null }, + message: 'must match exactly one schema in oneOf', + }, + ]; + + prepareResults(errors); + + expect(errors).toStrictEqual([ + { + keyword: 'type', + instancePath: '/paths/test/post/parameters/0/schema/type', + schemaPath: '#/properties/type/type', + params: { type: 'string' }, + message: 'must be string', + }, + ]); + }); + + test('given oneOf error one without any $ref property missing, picks all errors', () => { + const errors: ErrorObject[] = [ + { + keyword: 'type', + instancePath: '/paths/test/post/parameters/0/schema/type', + schemaPath: '#/properties/type/type', + params: { type: 'string' }, + message: 'must be string', + }, + { + keyword: 'type', + instancePath: '/paths/test/post/parameters/1/schema/type', + schemaPath: '#/properties/type/type', + params: { type: 'string' }, + message: 'must be string', + }, + { + keyword: 'oneOf', + instancePath: '/paths/test/post/parameters/0/schema', + schemaPath: '#/properties/schema/oneOf', + params: { passingSchemas: null }, + message: 'must match exactly one schema in oneOf', + }, + ]; + + prepareResults(errors); + + expect(errors).toStrictEqual([ + { + keyword: 'type', + instancePath: '/paths/test/post/parameters/0/schema/type', + schemaPath: '#/properties/type/type', + params: { type: 'string' }, + message: 'must be string', + }, + { + instancePath: '/paths/test/post/parameters/1/schema/type', + keyword: 'type', + message: 'must be string', + params: { + type: 'string', + }, + schemaPath: '#/properties/type/type', + }, + { + instancePath: '/paths/test/post/parameters/0/schema', + keyword: 'oneOf', + message: 'must match exactly one schema in oneOf', + params: { + passingSchemas: null, + }, + schemaPath: '#/properties/schema/oneOf', + }, + ]); + }); + + test('given errors with different data paths, picks all errors', () => { + const errors: ErrorObject[] = [ + { + keyword: 'type', + instancePath: '/paths/test/post/parameters/0/schema/type', + schemaPath: '#/properties/type/type', + params: { type: 'string' }, + message: 'must be string', + }, + { + keyword: 'required', + instancePath: '/paths/foo/post/parameters/0/schema', + schemaPath: '#/definitions/Reference/required', + params: { missingProperty: '$ref' }, + message: "must have required property '$ref'", + }, + { + keyword: 'oneOf', + instancePath: '/paths/baz/post/parameters/0/schema', + schemaPath: '#/properties/schema/oneOf', + params: { passingSchemas: null }, + message: 'must match exactly one schema in oneOf', + }, + ]; + + prepareResults(errors); + + expect(errors).toStrictEqual([ + { + instancePath: '/paths/test/post/parameters/0/schema/type', + keyword: 'type', + message: 'must be string', + params: { + type: 'string', + }, + schemaPath: '#/properties/type/type', + }, + { + instancePath: '/paths/foo/post/parameters/0/schema', + keyword: 'required', + message: "must have required property '$ref'", + params: { + missingProperty: '$ref', + }, + schemaPath: '#/definitions/Reference/required', + }, + { + instancePath: '/paths/baz/post/parameters/0/schema', + keyword: 'oneOf', + message: 'must match exactly one schema in oneOf', + params: { + passingSchemas: null, + }, + schemaPath: '#/properties/schema/oneOf', + }, + ]); + }); + }); +}); diff --git a/packages/rulesets/src/asyncapi/functions/asyncApi2DocumentSchema.ts b/packages/rulesets/src/asyncapi/functions/asyncApi2DocumentSchema.ts new file mode 100644 index 000000000..dceae2ed7 --- /dev/null +++ b/packages/rulesets/src/asyncapi/functions/asyncApi2DocumentSchema.ts @@ -0,0 +1,109 @@ +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 type { ErrorObject } from 'ajv'; +import type { IFunctionResult, Format } from '@stoplight/spectral-core'; + +// import only 2.X.X AsyncAPI JSON Schemas for better treeshaking +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'; + +function shouldIgnoreError(error: ErrorObject): boolean { + return ( + // oneOf is a fairly error as we have 2 options to choose from for most of the time. + error.keyword === 'oneOf' || + // the required $ref is entirely useless, since aas-schema rules operate on resolved content, so there won't be any $refs in the document + (error.keyword === 'required' && error.params.missingProperty === '$ref') + ); +} + +// this is supposed to cover edge cases we need to cover manually, when it's impossible to detect the most appropriate error, i.e. oneOf consisting of more than 3 members, etc. +// note, more errors can be included if certain messages reported by AJV are not quite meaningful +const ERROR_MAP = [ + { + path: /^components\/securitySchemes\/[^/]+$/, + message: 'Invalid security scheme', + }, +]; + +// The function removes irrelevant (aka misleading, confusing, useless, whatever you call it) errors. +// There are a few exceptions, i.e. security components I covered manually, +// yet apart from them we usually deal with a relatively simple scenario that can be literally expressed as: "either proper value of $ref property". +// The $ref part is never going to be interesting for us, because both aas-schema rules operate on resolved content, so we won't have any $refs left. +// As you can see, what we deal here wit is actually not really oneOf anymore - it's always the first member of oneOf we match against. +// That being said, we always strip both oneOf and $ref, since we are always interested in the first error. +export function prepareResults(errors: ErrorObject[]): void { + // Update additionalProperties errors to make them more precise and prevent them from being treated as duplicates + for (const error of errors) { + if (error.keyword === 'additionalProperties') { + error.instancePath = `${error.instancePath}/${String(error.params['additionalProperty'])}`; + } + } + + for (let i = 0; i < errors.length; i++) { + const error = errors[i]; + + if (i + 1 < errors.length && errors[i + 1].instancePath === error.instancePath) { + errors.splice(i + 1, 1); + i--; + } else if (i > 0 && shouldIgnoreError(error) && errors[i - 1].instancePath.startsWith(error.instancePath)) { + errors.splice(i, 1); + i--; + } + } +} + +function applyManualReplacements(errors: IFunctionResult[]): void { + for (const error of errors) { + if (error.path === void 0) continue; + + const joinedPath = error.path.join('/'); + + for (const mappedError of ERROR_MAP) { + if (mappedError.path.test(joinedPath)) { + error.message = mappedError.message; + break; + } + } + } +} + +function getSchema(formats: Set): Record | void { + switch (true) { + case formats.has(aas2_0): + return asyncAPI2_0_0Schema; + case formats.has(aas2_1): + return asyncAPI2_1_0Schema; + case formats.has(aas2_2): + return asyncAPI2_2_0Schema; + case formats.has(aas2_3): + return asyncAPI2_3_0Schema; + default: + return; + } +} + +export default createRulesetFunction( + { + input: null, + options: null, + }, + function oasDocumentSchema(targetVal, _, context) { + const formats = context.document.formats; + if (formats === null || formats === void 0) return; + + const schema = getSchema(formats); + if (schema === void 0) return; + + const errors = schemaFn(targetVal, { allErrors: true, schema, prepareResults }, context); + + if (Array.isArray(errors)) { + applyManualReplacements(errors); + } + + return errors; + }, +); diff --git a/packages/rulesets/src/asyncapi/functions/asyncApi2PayloadValidation.ts b/packages/rulesets/src/asyncapi/functions/asyncApi2PayloadValidation.ts index fec61d070..501ccc79f 100644 --- a/packages/rulesets/src/asyncapi/functions/asyncApi2PayloadValidation.ts +++ b/packages/rulesets/src/asyncapi/functions/asyncApi2PayloadValidation.ts @@ -2,10 +2,11 @@ import Ajv from 'ajv'; import addFormats from 'ajv-formats'; import { createRulesetFunction } from '@stoplight/spectral-core'; import betterAjvErrors from '@stoplight/better-ajv-errors'; -import * as asyncApi2Schema from '../schemas/schema.asyncapi2.json'; -const fakeSchemaObjectId = 'asyncapi2#/definitions/schema'; -const asyncApi2SchemaObject = { $ref: fakeSchemaObjectId }; +// use latest AsyncAPI JSON Schema because there are no differences of Schema Object definitions between the 2.X.X. +import * as asyncApi2Schema from '@asyncapi/specs/schemas/2.3.0.json'; + +const asyncApi2SchemaObject = { $ref: 'asyncapi2#/definitions/schema' }; const ajv = new Ajv({ allErrors: true, @@ -14,7 +15,7 @@ const ajv = new Ajv({ addFormats(ajv); -ajv.addSchema(asyncApi2Schema, asyncApi2Schema.$id); +ajv.addSchema(asyncApi2Schema, 'asyncapi2'); const ajvValidationFn = ajv.compile(asyncApi2SchemaObject); diff --git a/packages/rulesets/src/asyncapi/index.ts b/packages/rulesets/src/asyncapi/index.ts index 15c751c03..f6e83d7e8 100644 --- a/packages/rulesets/src/asyncapi/index.ts +++ b/packages/rulesets/src/asyncapi/index.ts @@ -1,4 +1,4 @@ -import { asyncApi2 } from '@stoplight/spectral-formats'; +import { aas2_0, aas2_1, aas2_2, aas2_3 } from '@stoplight/spectral-formats'; import { truthy, pattern, @@ -8,13 +8,13 @@ import { alphabetical, } from '@stoplight/spectral-functions'; +import asyncApi2DocumentSchema from './functions/asyncApi2DocumentSchema'; import asyncApi2SchemaValidation from './functions/asyncApi2SchemaValidation'; import asyncApi2PayloadValidation from './functions/asyncApi2PayloadValidation'; -import * as asyncApi2Schema from './schemas/schema.asyncapi2.json'; export default { documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md', - formats: [asyncApi2], + formats: [aas2_0, aas2_1, aas2_2, aas2_3], rules: { 'asyncapi-channel-no-empty-parameter': { description: 'Channel path must not have empty parameter substitution pattern.', @@ -272,18 +272,14 @@ export default { }, }, 'asyncapi-schema': { - description: 'Validate structure of AsyncAPI v2.0.0 Specification.', + description: 'Validate structure of AsyncAPI v2 specification.', message: '{{error}}', severity: 'error', recommended: true, type: 'validation', given: '$', then: { - function: schema, - functionOptions: { - allErrors: true, - schema: asyncApi2Schema, - }, + function: asyncApi2DocumentSchema, }, }, 'asyncapi-server-no-empty-variable': { diff --git a/packages/rulesets/src/asyncapi/schemas/schema.asyncapi2.json b/packages/rulesets/src/asyncapi/schemas/schema.asyncapi2.json deleted file mode 100644 index e07443707..000000000 --- a/packages/rulesets/src/asyncapi/schemas/schema.asyncapi2.json +++ /dev/null @@ -1,1486 +0,0 @@ -{ - "title": "AsyncAPI 2.0.0 schema.", - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "asyncapi2", - "type": "object", - "required": ["asyncapi", "info", "channels"], - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "asyncapi": { - "type": "string", - "enum": ["2.0.0"], - "description": "The AsyncAPI specification version of this document." - }, - "id": { - "type": "string", - "description": "A unique id representing the application.", - "format": "uri" - }, - "info": { - "$ref": "#/definitions/info" - }, - "servers": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/server" - } - }, - "defaultContentType": { - "type": "string" - }, - "channels": { - "$ref": "#/definitions/channels" - }, - "components": { - "$ref": "#/definitions/components" - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/definitions/tag" - }, - "uniqueItems": true - }, - "externalDocs": { - "$ref": "#/definitions/externalDocs" - } - }, - "definitions": { - "Reference": { - "type": "object", - "required": ["$ref"], - "properties": { - "$ref": { - "$ref": "#/definitions/ReferenceObject" - } - } - }, - "ReferenceObject": { - "type": "string", - "format": "uri-reference" - }, - "info": { - "type": "object", - "description": "General information about the API.", - "required": ["version", "title"], - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "title": { - "type": "string", - "description": "A unique and precise title of the API." - }, - "version": { - "type": "string", - "description": "A semantic version number of the API." - }, - "description": { - "type": "string", - "description": "A longer description of the API. Should be different from the title. CommonMark is allowed." - }, - "termsOfService": { - "type": "string", - "description": "A URL to the Terms of Service for the API. MUST be in the format of a URL.", - "format": "uri" - }, - "contact": { - "$ref": "#/definitions/contact" - }, - "license": { - "$ref": "#/definitions/license" - } - } - }, - "contact": { - "type": "object", - "description": "Contact information for the owners of the API.", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "description": "The identifying name of the contact person/organization." - }, - "url": { - "type": "string", - "description": "The URL pointing to the contact information.", - "format": "uri" - }, - "email": { - "type": "string", - "description": "The email address of the contact person/organization.", - "format": "email" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - } - }, - "license": { - "type": "object", - "required": ["name"], - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "description": "The name of the license type. It's encouraged to use an OSI compatible license." - }, - "url": { - "type": "string", - "description": "The URL pointing to the license.", - "format": "uri" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - } - }, - "server": { - "type": "object", - "description": "An object representing a Server.", - "required": ["url", "protocol"], - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "url": { - "type": "string" - }, - "description": { - "type": "string" - }, - "protocol": { - "type": "string", - "description": "The transfer protocol." - }, - "protocolVersion": { - "type": "string" - }, - "variables": { - "$ref": "#/definitions/serverVariables" - }, - "security": { - "type": "array", - "items": { - "$ref": "#/definitions/SecurityRequirement" - } - }, - "bindings": { - "$ref": "#/definitions/bindingsObject" - } - } - }, - "serverVariables": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/serverVariable" - } - }, - "serverVariable": { - "type": "object", - "description": "An object representing a Server Variable for server URL template substitution.", - "minProperties": 1, - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "enum": { - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "default": { - "type": "string" - }, - "description": { - "type": "string" - }, - "examples": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "channels": { - "type": "object", - "propertyNames": { - "type": "string", - "format": "uri-template", - "minLength": 1 - }, - "additionalProperties": { - "$ref": "#/definitions/channelItem" - } - }, - "components": { - "type": "object", - "description": "An object to hold a set of reusable objects for different aspects of the AsyncAPI Specification.", - "additionalProperties": false, - "properties": { - "schemas": { - "$ref": "#/definitions/schemas" - }, - "messages": { - "$ref": "#/definitions/messages" - }, - "securitySchemes": { - "type": "object", - "patternProperties": { - "^[\\w\\d.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/SecurityScheme" - } - ] - } - } - }, - "parameters": { - "$ref": "#/definitions/parameters" - }, - "correlationIds": { - "type": "object", - "patternProperties": { - "^[\\w\\d.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/correlationId" - } - ] - } - } - }, - "operationTraits": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/operationTrait" - } - }, - "messageTraits": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/messageTrait" - } - }, - "serverBindings": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/bindingsObject" - } - }, - "channelBindings": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/bindingsObject" - } - }, - "operationBindings": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/bindingsObject" - } - }, - "messageBindings": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/bindingsObject" - } - } - } - }, - "schemas": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/schema" - }, - "description": "JSON objects describing schemas the API uses." - }, - "messages": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/message" - }, - "description": "JSON objects describing the messages being consumed and produced by the API." - }, - "parameters": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/parameter" - }, - "description": "JSON objects describing re-usable channel parameters." - }, - "schema": { - "allOf": [ - { - "definitions": { - "schemaArray": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/definitions/schema/allOf/0" - } - }, - "nonNegativeInteger": { - "type": "integer", - "minimum": 0 - }, - "nonNegativeIntegerDefault0": { - "allOf": [ - { - "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeInteger" - }, - { - "default": 0 - } - ] - }, - "simpleTypes": { - "enum": ["array", "boolean", "integer", "null", "number", "object", "string"] - }, - "stringArray": { - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true, - "default": [] - } - }, - "type": ["object", "boolean"], - "properties": { - "$id": { - "type": "string", - "format": "uri-reference" - }, - "$schema": { - "type": "string", - "format": "uri" - }, - "$ref": { - "type": "string", - "format": "uri-reference" - }, - "$comment": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "default": true, - "readOnly": { - "type": "boolean", - "default": false - }, - "writeOnly": { - "type": "boolean", - "default": false - }, - "examples": { - "type": "array", - "items": true - }, - "multipleOf": { - "type": "number", - "exclusiveMinimum": 0 - }, - "maximum": { - "type": "number" - }, - "exclusiveMaximum": { - "type": "number" - }, - "minimum": { - "type": "number" - }, - "exclusiveMinimum": { - "type": "number" - }, - "maxLength": { - "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeInteger" - }, - "minLength": { - "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeIntegerDefault0" - }, - "pattern": { - "type": "string", - "format": "regex" - }, - "additionalItems": { - "$ref": "#/definitions/schema/allOf/0" - }, - "items": { - "anyOf": [ - { - "$ref": "#/definitions/schema/allOf/0" - }, - { - "$ref": "#/definitions/schema/allOf/0/definitions/schemaArray" - } - ], - "default": true - }, - "maxItems": { - "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeInteger" - }, - "minItems": { - "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeIntegerDefault0" - }, - "uniqueItems": { - "type": "boolean", - "default": false - }, - "contains": { - "$ref": "#/definitions/schema/allOf/0" - }, - "maxProperties": { - "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeInteger" - }, - "minProperties": { - "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeIntegerDefault0" - }, - "required": { - "$ref": "#/definitions/schema/allOf/0/definitions/stringArray" - }, - "additionalProperties": { - "$ref": "#/definitions/schema/allOf/0" - }, - "definitions": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/schema/allOf/0" - }, - "default": {} - }, - "properties": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/schema/allOf/0" - }, - "default": {} - }, - "patternProperties": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/schema/allOf/0" - }, - "propertyNames": { - "format": "regex" - }, - "default": {} - }, - "dependencies": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "$ref": "#/definitions/schema/allOf/0" - }, - { - "$ref": "#/definitions/schema/allOf/0/definitions/stringArray" - } - ] - } - }, - "propertyNames": { - "$ref": "#/definitions/schema/allOf/0" - }, - "const": true, - "enum": { - "type": "array", - "items": true, - "minItems": 1, - "uniqueItems": true - }, - "type": { - "anyOf": [ - { - "$ref": "#/definitions/schema/allOf/0/definitions/simpleTypes" - }, - { - "type": "array", - "items": { - "$ref": "#/definitions/schema/allOf/0/definitions/simpleTypes" - }, - "minItems": 1, - "uniqueItems": true - } - ] - }, - "format": { - "type": "string" - }, - "contentMediaType": { - "type": "string" - }, - "contentEncoding": { - "type": "string" - }, - "if": { - "$ref": "#/definitions/schema/allOf/0" - }, - "then": { - "$ref": "#/definitions/schema/allOf/0" - }, - "else": { - "$ref": "#/definitions/schema/allOf/0" - }, - "allOf": { - "$ref": "#/definitions/schema/allOf/0/definitions/schemaArray" - }, - "anyOf": { - "$ref": "#/definitions/schema/allOf/0/definitions/schemaArray" - }, - "oneOf": { - "$ref": "#/definitions/schema/allOf/0/definitions/schemaArray" - }, - "not": { - "$ref": "#/definitions/schema/allOf/0" - } - }, - "default": true - }, - { - "type": "object", - "patternProperties": { - "c": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "additionalProperties": { - "anyOf": [ - { - "$ref": "#/definitions/schema" - }, - { - "type": "boolean" - } - ], - "default": {} - }, - "items": { - "anyOf": [ - { - "$ref": "#/definitions/schema" - }, - { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/definitions/schema" - } - } - ], - "default": {} - }, - "allOf": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/definitions/schema" - } - }, - "oneOf": { - "type": "array", - "minItems": 2, - "items": { - "$ref": "#/definitions/schema" - } - }, - "anyOf": { - "type": "array", - "minItems": 2, - "items": { - "$ref": "#/definitions/schema" - } - }, - "not": { - "$ref": "#/definitions/schema" - }, - "properties": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/schema" - }, - "default": {} - }, - "patternProperties": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/schema" - }, - "default": {} - }, - "propertyNames": { - "$ref": "#/definitions/schema" - }, - "contains": { - "$ref": "#/definitions/schema" - }, - "discriminator": { - "type": "string" - }, - "externalDocs": { - "$ref": "#/definitions/externalDocs" - }, - "deprecated": { - "type": "boolean", - "default": false - } - } - } - ] - }, - "externalDocs": { - "type": "object", - "additionalProperties": false, - "description": "information about external documentation", - "required": ["url"], - "properties": { - "description": { - "type": "string" - }, - "url": { - "type": "string", - "format": "uri" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - } - }, - "channelItem": { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "minProperties": 1, - "properties": { - "$ref": { - "$ref": "#/definitions/ReferenceObject" - }, - "parameters": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/parameter" - } - }, - "description": { - "type": "string", - "description": "A description of the channel." - }, - "publish": { - "$ref": "#/definitions/operation" - }, - "subscribe": { - "$ref": "#/definitions/operation" - }, - "deprecated": { - "type": "boolean", - "default": false - }, - "bindings": { - "$ref": "#/definitions/bindingsObject" - } - } - }, - "parameter": { - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "description": { - "type": "string", - "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." - }, - "schema": { - "$ref": "#/definitions/schema" - }, - "location": { - "type": "string", - "description": "A runtime expression that specifies the location of the parameter value", - "pattern": "^\\$message\\.(header|payload)#(/(([^/~])|(~[01]))*)*" - }, - "$ref": { - "$ref": "#/definitions/ReferenceObject" - } - } - }, - "operation": { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "traits": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/operationTrait" - }, - { - "type": "array", - "items": [ - { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/operationTrait" - } - ] - }, - { - "type": "object", - "additionalItems": true - } - ] - } - ] - } - }, - "summary": { - "type": "string" - }, - "description": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/definitions/tag" - }, - "uniqueItems": true - }, - "externalDocs": { - "$ref": "#/definitions/externalDocs" - }, - "operationId": { - "type": "string" - }, - "bindings": { - "$ref": "#/definitions/bindingsObject" - }, - "message": { - "$ref": "#/definitions/message" - } - } - }, - "message": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "oneOf": [ - { - "type": "object", - "required": ["oneOf"], - "additionalProperties": false, - "properties": { - "oneOf": { - "type": "array", - "items": { - "$ref": "#/definitions/message" - } - } - } - }, - { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "schemaFormat": { - "type": "string" - }, - "contentType": { - "type": "string" - }, - "headers": { - "$ref": "#/definitions/schema" - }, - "payload": {}, - "correlationId": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/correlationId" - } - ] - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/definitions/tag" - }, - "uniqueItems": true - }, - "summary": { - "type": "string", - "description": "A brief summary of the message." - }, - "name": { - "type": "string", - "description": "Name of the message." - }, - "title": { - "type": "string", - "description": "A human-friendly title for the message." - }, - "description": { - "type": "string", - "description": "A longer description of the message. CommonMark is allowed." - }, - "externalDocs": { - "$ref": "#/definitions/externalDocs" - }, - "deprecated": { - "type": "boolean", - "default": false - }, - "examples": { - "type": "array", - "items": { - "type": "object" - } - }, - "bindings": { - "$ref": "#/definitions/bindingsObject" - }, - "traits": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/messageTrait" - }, - { - "type": "array", - "items": [ - { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/messageTrait" - } - ] - }, - { - "type": "object", - "additionalItems": true - } - ] - } - ] - } - } - } - } - ] - } - ] - }, - "bindingsObject": { - "type": "object", - "additionalProperties": true, - "properties": { - "http": {}, - "ws": {}, - "amqp": {}, - "amqp1": {}, - "mqtt": {}, - "mqtt5": {}, - "kafka": {}, - "nats": {}, - "jms": {}, - "sns": {}, - "sqs": {}, - "stomp": {}, - "redis": {} - } - }, - "correlationId": { - "type": "object", - "required": ["location"], - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "description": { - "type": "string", - "description": "A optional description of the correlation ID. GitHub Flavored Markdown is allowed." - }, - "location": { - "type": "string", - "description": "A runtime expression that specifies the location of the correlation ID", - "pattern": "^\\$message\\.(header|payload)#(/(([^/~])|(~[01]))*)*" - } - } - }, - "specificationExtension": { - "description": "Any property starting with x- is valid.", - "additionalProperties": true, - "additionalItems": true - }, - "tag": { - "type": "object", - "additionalProperties": false, - "required": ["name"], - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "externalDocs": { - "$ref": "#/definitions/externalDocs" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - } - }, - "operationTrait": { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "summary": { - "type": "string" - }, - "description": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/definitions/tag" - }, - "uniqueItems": true - }, - "externalDocs": { - "$ref": "#/definitions/externalDocs" - }, - "operationId": { - "type": "string" - }, - "bindings": { - "$ref": "#/definitions/bindingsObject" - } - } - }, - "messageTrait": { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "schemaFormat": { - "type": "string" - }, - "contentType": { - "type": "string" - }, - "headers": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/schema" - } - ] - }, - "correlationId": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/correlationId" - } - ] - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/definitions/tag" - }, - "uniqueItems": true - }, - "summary": { - "type": "string", - "description": "A brief summary of the message." - }, - "name": { - "type": "string", - "description": "Name of the message." - }, - "title": { - "type": "string", - "description": "A human-friendly title for the message." - }, - "description": { - "type": "string", - "description": "A longer description of the message. CommonMark is allowed." - }, - "externalDocs": { - "$ref": "#/definitions/externalDocs" - }, - "deprecated": { - "type": "boolean", - "default": false - }, - "examples": { - "type": "array", - "items": { - "type": "object" - } - }, - "bindings": { - "$ref": "#/definitions/bindingsObject" - } - } - }, - "SecurityScheme": { - "oneOf": [ - { - "$ref": "#/definitions/userPassword" - }, - { - "$ref": "#/definitions/apiKey" - }, - { - "$ref": "#/definitions/X509" - }, - { - "$ref": "#/definitions/symmetricEncryption" - }, - { - "$ref": "#/definitions/asymmetricEncryption" - }, - { - "$ref": "#/definitions/HTTPSecurityScheme" - }, - { - "$ref": "#/definitions/oauth2Flows" - }, - { - "$ref": "#/definitions/openIdConnect" - } - ] - }, - "userPassword": { - "type": "object", - "required": ["type"], - "properties": { - "type": { - "type": "string", - "enum": ["userPassword"] - }, - "description": { - "type": "string" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "apiKey": { - "type": "object", - "required": ["type", "in"], - "properties": { - "type": { - "type": "string", - "enum": ["apiKey"] - }, - "in": { - "type": "string", - "enum": ["user", "password"] - }, - "description": { - "type": "string" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "X509": { - "type": "object", - "required": ["type"], - "properties": { - "type": { - "type": "string", - "enum": ["X509"] - }, - "description": { - "type": "string" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "symmetricEncryption": { - "type": "object", - "required": ["type"], - "properties": { - "type": { - "type": "string", - "enum": ["symmetricEncryption"] - }, - "description": { - "type": "string" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "asymmetricEncryption": { - "type": "object", - "required": ["type"], - "properties": { - "type": { - "type": "string", - "enum": ["asymmetricEncryption"] - }, - "description": { - "type": "string" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "HTTPSecurityScheme": { - "oneOf": [ - { - "$ref": "#/definitions/NonBearerHTTPSecurityScheme" - }, - { - "$ref": "#/definitions/BearerHTTPSecurityScheme" - }, - { - "$ref": "#/definitions/APIKeyHTTPSecurityScheme" - } - ] - }, - "NonBearerHTTPSecurityScheme": { - "not": { - "type": "object", - "properties": { - "scheme": { - "type": "string", - "enum": ["bearer"] - } - } - }, - "type": "object", - "required": ["scheme", "type"], - "properties": { - "scheme": { - "type": "string" - }, - "description": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["http"] - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "BearerHTTPSecurityScheme": { - "type": "object", - "required": ["type", "scheme"], - "properties": { - "scheme": { - "type": "string", - "enum": ["bearer"] - }, - "bearerFormat": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["http"] - }, - "description": { - "type": "string" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "APIKeyHTTPSecurityScheme": { - "type": "object", - "required": ["type", "name", "in"], - "properties": { - "type": { - "type": "string", - "enum": ["httpApiKey"] - }, - "name": { - "type": "string" - }, - "in": { - "type": "string", - "enum": ["header", "query", "cookie"] - }, - "description": { - "type": "string" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "oauth2Flows": { - "type": "object", - "required": ["type", "flows"], - "properties": { - "type": { - "type": "string", - "enum": ["oauth2"] - }, - "description": { - "type": "string" - }, - "flows": { - "type": "object", - "properties": { - "implicit": { - "allOf": [ - { - "$ref": "#/definitions/oauth2Flow" - }, - { - "required": ["authorizationUrl", "scopes"] - }, - { - "not": { - "required": ["tokenUrl"] - } - } - ] - }, - "password": { - "allOf": [ - { - "$ref": "#/definitions/oauth2Flow" - }, - { - "required": ["tokenUrl", "scopes"] - }, - { - "not": { - "required": ["authorizationUrl"] - } - } - ] - }, - "clientCredentials": { - "allOf": [ - { - "$ref": "#/definitions/oauth2Flow" - }, - { - "required": ["tokenUrl", "scopes"] - }, - { - "not": { - "required": ["authorizationUrl"] - } - } - ] - }, - "authorizationCode": { - "allOf": [ - { - "$ref": "#/definitions/oauth2Flow" - }, - { - "required": ["authorizationUrl", "tokenUrl", "scopes"] - } - ] - } - }, - "additionalProperties": false, - "minProperties": 1 - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - } - }, - "oauth2Flow": { - "type": "object", - "properties": { - "authorizationUrl": { - "type": "string", - "format": "uri" - }, - "tokenUrl": { - "type": "string", - "format": "uri" - }, - "refreshUrl": { - "type": "string", - "format": "uri" - }, - "scopes": { - "$ref": "#/definitions/oauth2Scopes" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "oauth2Scopes": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "openIdConnect": { - "type": "object", - "required": ["type", "openIdConnectUrl"], - "properties": { - "type": { - "type": "string", - "enum": ["openIdConnect"] - }, - "description": { - "type": "string" - }, - "openIdConnectUrl": { - "type": "string", - "format": "uri" - } - }, - "patternProperties": { - "^x-[\\w\\d.\\-_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "additionalProperties": false - }, - "SecurityRequirement": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - } - } - } -} diff --git a/packages/rulesets/src/index.ts b/packages/rulesets/src/index.ts index f85f7a30b..8f5b53792 100644 --- a/packages/rulesets/src/index.ts +++ b/packages/rulesets/src/index.ts @@ -1,2 +1,4 @@ -export { default as oas } from './oas'; -export { default as asyncapi } from './asyncapi'; +import { default as oas } from './oas'; +import { default as asyncapi } from './asyncapi'; + +export { oas, asyncapi }; diff --git a/packages/rulesets/src/oas/__tests__/oas2-operation-security-defined.test.ts b/packages/rulesets/src/oas/__tests__/oas2-operation-security-defined.test.ts index df1431246..92d714ac5 100644 --- a/packages/rulesets/src/oas/__tests__/oas2-operation-security-defined.test.ts +++ b/packages/rulesets/src/oas/__tests__/oas2-operation-security-defined.test.ts @@ -5,6 +5,7 @@ testRule('oas2-operation-security-defined', [ { name: 'a correct object (just in body)', document: { + swagger: '2.0', securityDefinitions: { apikey: {}, }, @@ -23,6 +24,27 @@ testRule('oas2-operation-security-defined', [ errors: [], }, + { + name: 'a correct object (API-level security)', + document: { + swagger: '2.0', + securityDefinitions: { + apikey: {}, + }, + security: [ + { + apikey: [], + }, + ], + paths: { + '/path': { + get: {}, + }, + }, + }, + errors: [], + }, + { name: 'invalid object', document: { @@ -43,7 +65,89 @@ testRule('oas2-operation-security-defined', [ errors: [ { message: 'Operation "security" values must match a scheme defined in the "securityDefinitions" object.', - path: ['paths', '/path', 'get', 'security', '0'], + path: ['paths', '/path', 'get', 'security', '0', 'apikey'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + + { + name: 'invalid object (API-level security)', + document: { + swagger: '2.0', + securityDefinitions: {}, + security: [ + { + apikey: [], + }, + ], + paths: { + '/path': { + get: {}, + }, + }, + }, + errors: [ + { + message: 'API "security" values must match a scheme defined in the "securityDefinitions" object.', + path: ['security', '0', 'apikey'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + + { + name: 'valid and invalid object', + document: { + swagger: '2.0', + securityDefinitions: { + apikey: {}, + }, + paths: { + '/path': { + get: { + security: [ + { + apikey: [], + basic: [], + }, + ], + }, + }, + }, + }, + errors: [ + { + message: 'Operation "security" values must match a scheme defined in the "securityDefinitions" object.', + path: ['paths', '/path', 'get', 'security', '0', 'basic'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + + { + name: 'valid and invalid object (API-level security)', + document: { + swagger: '2.0', + securityDefinitions: { + apikey: {}, + }, + security: [ + { + apikey: [], + basic: [], + }, + ], + paths: { + '/path': { + get: {}, + }, + }, + }, + errors: [ + { + message: 'API "security" values must match a scheme defined in the "securityDefinitions" object.', + path: ['security', '0', 'basic'], severity: DiagnosticSeverity.Warning, }, ], diff --git a/packages/rulesets/src/oas/__tests__/oas3-operation-security-defined.test.ts b/packages/rulesets/src/oas/__tests__/oas3-operation-security-defined.test.ts index ea518c723..f3d7f5212 100644 --- a/packages/rulesets/src/oas/__tests__/oas3-operation-security-defined.test.ts +++ b/packages/rulesets/src/oas/__tests__/oas3-operation-security-defined.test.ts @@ -25,6 +25,28 @@ testRule('oas3-operation-security-defined', [ }, errors: [], }, + { + name: 'validate a correct object (API-level security)', + document: { + openapi: '3.0.2', + components: { + securitySchemes: { + apikey: {}, + }, + security: [ + { + apikey: [], + }, + ], + }, + paths: { + '/path': { + get: {}, + }, + }, + }, + errors: [], + }, { name: 'return errors on invalid object', @@ -46,7 +68,93 @@ testRule('oas3-operation-security-defined', [ errors: [ { message: 'Operation "security" values must match a scheme defined in the "components.securitySchemes" object.', - path: ['paths', '/path', 'get', 'security', '0'], + path: ['paths', '/path', 'get', 'security', '0', 'apikey'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + + { + name: 'return errors on invalid object (API-level)', + document: { + openapi: '3.0.2', + components: {}, + security: [ + { + apikey: [], + }, + ], + paths: { + '/path': { + get: {}, + }, + }, + }, + errors: [ + { + message: 'API "security" values must match a scheme defined in the "components.securitySchemes" object.', + path: ['security', '0', 'apikey'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + + { + name: 'return errors on valid and invalid object', + document: { + openapi: '3.0.2', + components: { + securitySchemes: { + apikey: {}, + }, + }, + paths: { + '/path': { + get: { + security: [ + { + apikey: [], + basic: [], + }, + ], + }, + }, + }, + }, + errors: [ + { + message: 'Operation "security" values must match a scheme defined in the "components.securitySchemes" object.', + path: ['paths', '/path', 'get', 'security', '0', 'basic'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + + { + name: 'valid and invalid object (API-level security)', + document: { + openapi: '3.0.2', + components: { + securitySchemes: { + apikey: {}, + }, + }, + security: [ + { + apikey: [], + basic: [], + }, + ], + paths: { + '/path': { + get: {}, + }, + }, + }, + errors: [ + { + message: 'API "security" values must match a scheme defined in the "components.securitySchemes" object.', + path: ['security', '0', 'basic'], severity: DiagnosticSeverity.Warning, }, ], diff --git a/packages/rulesets/src/oas/functions/index.ts b/packages/rulesets/src/oas/functions/index.ts index 2fe7c0a20..430d8d9a8 100644 --- a/packages/rulesets/src/oas/functions/index.ts +++ b/packages/rulesets/src/oas/functions/index.ts @@ -1,14 +1,31 @@ -export { default as oasOpParams } from './oasOpParams'; -export { default as oasSchema } from './oasSchema'; -export { default as oasDocumentSchema } from './oasDocumentSchema'; -export { default as oasOpFormDataConsumeCheck } from './oasOpFormDataConsumeCheck'; -export { default as oasOpSuccessResponse } from './oasOpSuccessResponse'; -export { default as oasExample } from './oasExample'; -export { default as oasOpSecurityDefined } from './oasOpSecurityDefined'; -export { default as typedEnum } from './typedEnum'; -export { default as refSiblings } from './refSiblings'; -export { default as oasPathParam } from './oasPathParam'; -export { default as oasTagDefined } from './oasTagDefined'; -export { default as oasUnusedComponent } from './oasUnusedComponent'; -export { default as oasOpIdUnique } from './oasOpIdUnique'; -export { default as oasDiscriminator } from './oasDiscriminator'; +import { default as oasOpParams } from './oasOpParams'; +import { default as oasSchema } from './oasSchema'; +import { default as oasDocumentSchema } from './oasDocumentSchema'; +import { default as oasOpFormDataConsumeCheck } from './oasOpFormDataConsumeCheck'; +import { default as oasOpSuccessResponse } from './oasOpSuccessResponse'; +import { default as oasExample } from './oasExample'; +import { default as oasOpSecurityDefined } from './oasOpSecurityDefined'; +import { default as typedEnum } from './typedEnum'; +import { default as refSiblings } from './refSiblings'; +import { default as oasPathParam } from './oasPathParam'; +import { default as oasTagDefined } from './oasTagDefined'; +import { default as oasUnusedComponent } from './oasUnusedComponent'; +import { default as oasOpIdUnique } from './oasOpIdUnique'; +import { default as oasDiscriminator } from './oasDiscriminator'; + +export { + oasOpParams, + oasSchema, + oasDocumentSchema, + oasOpFormDataConsumeCheck, + oasOpSuccessResponse, + oasExample, + oasOpSecurityDefined, + typedEnum, + refSiblings, + oasPathParam, + oasTagDefined, + oasUnusedComponent, + oasOpIdUnique, + oasDiscriminator, +}; diff --git a/packages/rulesets/src/oas/functions/oasOpSecurityDefined.ts b/packages/rulesets/src/oas/functions/oasOpSecurityDefined.ts index 31f42c104..37edd2eca 100644 --- a/packages/rulesets/src/oas/functions/oasOpSecurityDefined.ts +++ b/packages/rulesets/src/oas/functions/oasOpSecurityDefined.ts @@ -20,7 +20,7 @@ type Options = { schemesPath: JsonPath; }; -export default createRulesetFunction<{ paths: Record }, Options>( +export default createRulesetFunction<{ paths: Record; security: unknown[] }, Options>( { input: { type: 'object', @@ -28,6 +28,9 @@ export default createRulesetFunction<{ paths: Record }, Options paths: { type: 'object', }, + security: { + type: 'array', + }, }, }, options: { @@ -50,6 +53,29 @@ export default createRulesetFunction<{ paths: Record }, Options const schemes = _get(targetVal, schemesPath); const allDefs = isObject(schemes) ? Object.keys(schemes) : []; + // Check global security requirements + + const { security } = targetVal; + + if (Array.isArray(security)) { + for (const [index, value] of security.entries()) { + if (!isObject(value)) { + continue; + } + + const securityKeys = Object.keys(value); + + for (const securityKey of securityKeys) { + if (!allDefs.includes(securityKey)) { + results.push({ + message: `API "security" values must match a scheme defined in the "${schemesPath.join('.')}" object.`, + path: ['security', index, securityKey], + }); + } + } + } + } + for (const { path, operation, value } of getAllOperations(paths)) { if (!isObject(value)) continue; @@ -66,11 +92,15 @@ export default createRulesetFunction<{ paths: Record }, Options const securityKeys = Object.keys(value); - if (securityKeys.length > 0 && !allDefs.includes(securityKeys[0])) { - results.push({ - message: 'Operation must not reference an undefined security scheme.', - path: ['paths', path, operation, 'security', index], - }); + for (const securityKey of securityKeys) { + if (!allDefs.includes(securityKey)) { + results.push({ + message: `Operation "security" values must match a scheme defined in the "${schemesPath.join( + '.', + )}" object.`, + path: ['paths', path, operation, 'security', index, securityKey], + }); + } } } } diff --git a/packages/rulesets/src/oas/index.ts b/packages/rulesets/src/oas/index.ts index 6e7a4d4fe..8d7e817ff 100644 --- a/packages/rulesets/src/oas/index.ts +++ b/packages/rulesets/src/oas/index.ts @@ -449,6 +449,7 @@ const ruleset = { }, 'oas2-operation-security-defined': { description: 'Operation "security" values must match a scheme defined in the "securityDefinitions" object.', + message: '{{error}}', recommended: true, formats: [oas2], type: 'validation', @@ -590,6 +591,7 @@ const ruleset = { 'oas3-operation-security-defined': { description: 'Operation "security" values must match a scheme defined in the "components.securitySchemes" object.', + message: '{{error}}', recommended: true, formats: [oas3], type: 'validation', diff --git a/yarn.lock b/yarn.lock index d0e543a08..c809499b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,6 +17,13 @@ __metadata: languageName: node linkType: hard +"@asyncapi/specs@npm:^2.13.0": + version: 2.13.0 + resolution: "@asyncapi/specs@npm:2.13.0" + checksum: 94355c96ac2562bfd9118a3e33dd36359196d070684da952f6b0f800b588b426d012fbf96f85b7341ec74f401e3487934b92e43f48e086fa956eab29b90ab694 + languageName: node + linkType: hard + "@babel/code-frame@npm:7.12.11": version: 7.12.11 resolution: "@babel/code-frame@npm:7.12.11" @@ -2070,6 +2077,23 @@ __metadata: languageName: node linkType: hard +"@rollup/plugin-commonjs@npm:^21.0.1": + version: 21.0.1 + resolution: "@rollup/plugin-commonjs@npm:21.0.1" + dependencies: + "@rollup/pluginutils": ^3.1.0 + commondir: ^1.0.1 + estree-walker: ^2.0.1 + glob: ^7.1.6 + is-reference: ^1.2.1 + magic-string: ^0.25.7 + resolve: ^1.17.0 + peerDependencies: + rollup: ^2.38.3 + checksum: 3e56be58c72d655face6f361f85923ddcc3cc07760b5a3a91cfc728115dfed358fc595781148c512d53a03be8c703133379f228e78fd2aed8655fae9d83800b6 + languageName: node + linkType: hard + "@rollup/pluginutils@npm:^3.1.0": version: 3.1.0 resolution: "@rollup/pluginutils@npm:3.1.0" @@ -2172,9 +2196,9 @@ __metadata: languageName: node linkType: hard -"@semantic-release/npm@npm:^9.0.0": - version: 9.0.0 - resolution: "@semantic-release/npm@npm:9.0.0" +"@semantic-release/npm@npm:^8.0.0": + version: 8.0.3 + resolution: "@semantic-release/npm@npm:8.0.3" dependencies: "@semantic-release/error": ^3.0.0 aggregate-error: ^3.0.0 @@ -2183,15 +2207,15 @@ __metadata: lodash: ^4.17.15 nerf-dart: ^1.0.0 normalize-url: ^6.0.0 - npm: ^8.3.0 + npm: ^7.0.0 rc: ^1.2.8 read-pkg: ^5.0.0 registry-auth-token: ^4.0.0 semver: ^7.1.2 tempy: ^1.0.0 peerDependencies: - semantic-release: ">=19.0.0" - checksum: e5cbb66702d9c7030b7e03f0f74764b321fc3ee6d151207180874df933643eb6a4b4f29eec130bbe1521190c169a6c36813afaa853365fb7affd8d6d7642f69a + semantic-release: ">=18.0.0" + checksum: 6c1e178f0fdc1b6ab24d14f02fb012302c7220e64e192293be7d11346d309b00338bd5a42e076c7849af68a91745359e750c5a1d7c85c8f11e9941e4516bb413 languageName: node linkType: hard @@ -2388,7 +2412,7 @@ __metadata: languageName: unknown linkType: soft -"@stoplight/spectral-formats@*, @stoplight/spectral-formats@>=1, @stoplight/spectral-formats@^1.0.0, @stoplight/spectral-formats@^1.0.2, @stoplight/spectral-formats@workspace:packages/formats": +"@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": version: 0.0.0-use.local resolution: "@stoplight/spectral-formats@workspace:packages/formats" dependencies: @@ -2446,6 +2470,7 @@ __metadata: version: 0.0.0-use.local resolution: "@stoplight/spectral-ruleset-bundler@workspace:packages/ruleset-bundler" dependencies: + "@rollup/plugin-commonjs": ^21.0.1 "@stoplight/path": 1.3.2 "@stoplight/spectral-core": ">=1" "@stoplight/spectral-formats": ">=1" @@ -2462,7 +2487,7 @@ __metadata: memfs: ^3.3.0 pony-cause: 1.1.1 prettier: ^2.4.1 - rollup: ~2.60.2 + rollup: ~2.67.0 tslib: ^2.3.1 validate-npm-package-name: 3.0.0 languageName: unknown @@ -2499,11 +2524,12 @@ __metadata: version: 0.0.0-use.local resolution: "@stoplight/spectral-rulesets@workspace:packages/rulesets" dependencies: + "@asyncapi/specs": ^2.13.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.0.2 + "@stoplight/spectral-formats": ^1.1.0 "@stoplight/spectral-functions": ^1.5.1 "@stoplight/spectral-parsers": "*" "@stoplight/spectral-ref-resolver": "*" @@ -3235,7 +3261,7 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.0": +"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.0, ansi-escapes@npm:^4.3.1": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2" dependencies: @@ -3244,15 +3270,6 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^5.0.0": - version: 5.0.0 - resolution: "ansi-escapes@npm:5.0.0" - dependencies: - type-fest: ^1.0.2 - checksum: d4b5eb8207df38367945f5dd2ef41e08c28edc192dc766ef18af6b53736682f49d8bfcfa4e4d6ecbc2e2f97c258fda084fb29a9e43b69170b71090f771afccac - languageName: node - linkType: hard - "ansi-regex@npm:^2.0.0, ansi-regex@npm:^2.1.1": version: 2.1.1 resolution: "ansi-regex@npm:2.1.1" @@ -4017,7 +4034,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:*, chalk@npm:^5.0.0": +"chalk@npm:*": version: 5.0.0 resolution: "chalk@npm:5.0.0" checksum: 6eba7c518b9aa5fe882ae6d14a1ffa58c418d72a3faa7f72af56641f1bbef51b645fca1d6e05d42357b7d3c846cd504c0b7b64d12309cdd07867e3b4411e0d01 @@ -4165,7 +4182,7 @@ __metadata: languageName: node linkType: hard -"cli-table3@npm:*, cli-table3@npm:^0.6.1": +"cli-table3@npm:*, cli-table3@npm:^0.6.0": version: 0.6.1 resolution: "cli-table3@npm:0.6.1" dependencies: @@ -8861,28 +8878,28 @@ __metadata: languageName: node linkType: hard -"marked-terminal@npm:^5.0.0": - version: 5.1.1 - resolution: "marked-terminal@npm:5.1.1" +"marked-terminal@npm:^4.1.1": + version: 4.2.0 + resolution: "marked-terminal@npm:4.2.0" dependencies: - ansi-escapes: ^5.0.0 + ansi-escapes: ^4.3.1 cardinal: ^2.1.1 - chalk: ^5.0.0 - cli-table3: ^0.6.1 - node-emoji: ^1.11.0 - supports-hyperlinks: ^2.2.0 + chalk: ^4.1.0 + cli-table3: ^0.6.0 + node-emoji: ^1.10.0 + supports-hyperlinks: ^2.1.0 peerDependencies: - marked: ^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 - checksum: 24ceb02ebd10e9c6c2fac2240a2cc019093c95029732779ea41ba7a81c45867e956d1f6f1ae7426d5247ab5185b9cdaea31a9663e4d624c17335660fa9474c3d + marked: ^1.0.0 || ^2.0.0 + checksum: a68a4cfd22b42f990a82e3234c68006ab4d1285a4a9bdd162f597740d9a55275c10c78ca21fa3927a76b2197589fe382e33af9baa2ccb2153812986c15aa73b8 languageName: node linkType: hard -"marked@npm:^4.0.10": - version: 4.0.12 - resolution: "marked@npm:4.0.12" +"marked@npm:^2.0.0": + version: 2.1.3 + resolution: "marked@npm:2.1.3" bin: - marked: bin/marked.js - checksum: 7575117f85a8986652f3ac8b8a7b95056c4c5fce01a1fc76dc4c7960412cb4c9bd9da8133487159b6b3ff84f52b543dfe9a36f826a5f358892b5ec4b6824f192 + marked: bin/marked + checksum: 21a5ecd4941bc760aba21dfd97185853ec3b464cf707ad971e3ddb3aeb2f44d0deeb36b0889932afdb6f734975a994d92f18815dd0fabadbd902bdaff997cc5b languageName: node linkType: hard @@ -9360,7 +9377,7 @@ __metadata: languageName: node linkType: hard -"node-emoji@npm:^1.11.0": +"node-emoji@npm:^1.10.0": version: 1.11.0 resolution: "node-emoji@npm:1.11.0" dependencies: @@ -9649,9 +9666,9 @@ __metadata: languageName: node linkType: hard -"npm@npm:^8.3.0": - version: 8.4.0 - resolution: "npm@npm:8.4.0" +"npm@npm:^7.0.0": + version: 7.24.2 + resolution: "npm@npm:7.24.2" dependencies: "@isaacs/string-locale-compare": "*" "@npmcli/arborist": "*" @@ -9708,7 +9725,6 @@ __metadata: opener: "*" pacote: "*" parse-conflict-json: "*" - proc-log: "*" qrcode-terminal: "*" read: "*" read-package-json: "*" @@ -9727,7 +9743,7 @@ __metadata: bin: npm: bin/npm-cli.js npx: bin/npx-cli.js - checksum: eb2b78dec31016441adbbf2c708569b99f24bbc004449de0d95c43b0b664a28cf95b368716ac7841c45b3454e2b3772b81c620eb742a42a73851080fab3a4101 + checksum: 8d818fd4f8394a24147d1b5ec8395f96c443fea18c54238ab2e842b8d86d977da98d0ab124744161d2bc7a5b8edbc21b6c0c1117e76e68d2c5ee24c8a4f39381 languageName: node linkType: hard @@ -10489,7 +10505,7 @@ __metadata: languageName: node linkType: hard -"proc-log@npm:*, proc-log@npm:^1.0.0": +"proc-log@npm:^1.0.0": version: 1.0.0 resolution: "proc-log@npm:1.0.0" checksum: 249605d5b28bfa0499d70da24ab056ad1e082a301f0a46d0ace6e8049cf16aaa0e71d9ea5cab29b620ffb327c18af97f0e012d1db090673447e7c1d33239dd96 @@ -11154,9 +11170,9 @@ __metadata: languageName: node linkType: hard -"rollup@npm:~2.60.2": - version: 2.60.2 - resolution: "rollup@npm:2.60.2" +"rollup@npm:~2.67.0": + version: 2.67.2 + resolution: "rollup@npm:2.67.2" dependencies: fsevents: ~2.3.2 dependenciesMeta: @@ -11164,7 +11180,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: bcd41dfe8afb7e0d97ce2237752165bdda689bcce6321d96821d565de3e0c865a49b544923f315985be2bfde086f72b54aae4ae7c87f798b3cb9558a5bec4e65 + checksum: 9aca5251ba4b441064183cde2394b91567259002d68086bdd3906db66d55dd148ab27e57c51eb53830d7b9b813c2d4e834b7735d65e2a869780bc639d4a20c38 languageName: node linkType: hard @@ -11210,7 +11226,7 @@ __metadata: node-powershell: ^4.0.0 patch-package: ^6.4.7 prettier: ^2.4.1 - semantic-release: ^19.0.2 + semantic-release: ^18.0.1 semantic-release-monorepo: ^7.0.5 ts-jest: ^27.0.7 ts-node: ^10.4.0 @@ -11304,14 +11320,14 @@ __metadata: languageName: node linkType: hard -"semantic-release@npm:^19.0.2": - version: 19.0.2 - resolution: "semantic-release@npm:19.0.2" +"semantic-release@npm:^18.0.1": + version: 18.0.1 + resolution: "semantic-release@npm:18.0.1" dependencies: "@semantic-release/commit-analyzer": ^9.0.2 "@semantic-release/error": ^3.0.0 "@semantic-release/github": ^8.0.0 - "@semantic-release/npm": ^9.0.0 + "@semantic-release/npm": ^8.0.0 "@semantic-release/release-notes-generator": ^10.0.0 aggregate-error: ^3.0.0 cosmiconfig: ^7.0.0 @@ -11325,8 +11341,8 @@ __metadata: hook-std: ^2.0.0 hosted-git-info: ^4.0.0 lodash: ^4.17.21 - marked: ^4.0.10 - marked-terminal: ^5.0.0 + marked: ^2.0.0 + marked-terminal: ^4.1.1 micromatch: ^4.0.2 p-each-series: ^2.1.0 p-reduce: ^2.0.0 @@ -11338,7 +11354,7 @@ __metadata: yargs: ^16.2.0 bin: semantic-release: bin/semantic-release.js - checksum: 0807cae8c57445793d3181a15cd587950aaf6b9c6ea9f4b7876b85a4ac78d1cd8d53f309512fe53eca2a8ed48600dd4d5483ac403bb42bfcf1c88a2c2340cf65 + checksum: e99634d2fd392d007cd83cc28318cd4b0781825b550e75486676941b8f67a32c1b907c53de2761440b38ead220629cc3778c22373aacce4ee291dba43971b0d6 languageName: node linkType: hard @@ -12048,7 +12064,7 @@ __metadata: languageName: node linkType: hard -"supports-hyperlinks@npm:^2.0.0, supports-hyperlinks@npm:^2.2.0": +"supports-hyperlinks@npm:^2.0.0, supports-hyperlinks@npm:^2.1.0": version: 2.2.0 resolution: "supports-hyperlinks@npm:2.2.0" dependencies: @@ -12577,13 +12593,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^1.0.2": - version: 1.4.0 - resolution: "type-fest@npm:1.4.0" - checksum: b011c3388665b097ae6a109a437a04d6f61d81b7357f74cbcb02246f2f5bd72b888ae33631b99871388122ba0a87f4ff1c94078e7119ff22c70e52c0ff828201 - languageName: node - linkType: hard - "type-is@npm:~1.6.17": version: 1.6.18 resolution: "type-is@npm:1.6.18"