diff --git a/jest.config.js b/jest.config.js index e2c30f16d..481190024 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,9 +8,7 @@ const projectDefault = { preset: 'ts-jest', moduleNameMapper: { ...mapValues(pathsToModuleNameMapper(compilerOptions.paths), v => path.join(__dirname, v)), - '^@stoplight/spectral-test-utils$': '/test-utils/node/index.ts', - '^nimma/fallbacks$': '/node_modules/nimma/dist/cjs/fallbacks/index.js', - '^nimma/legacy$': '/node_modules/nimma/dist/legacy/cjs/index.js', + '^@stoplight/spectral\\-test\\-utils$': '/test-utils/node/index.ts', }, testEnvironment: 'node', globals: { diff --git a/karma.conf.ts b/karma.conf.ts index 45917c494..2458c466e 100644 --- a/karma.conf.ts +++ b/karma.conf.ts @@ -1,6 +1,7 @@ // Karma configuration // Generated on Tue Jul 02 2019 17:18:30 GMT+0200 (Central European Summer Time) +import * as path from 'path'; import type { TransformCallback, TransformContext } from 'karma-typescript'; import type { Config } from 'karma'; @@ -14,7 +15,7 @@ module.exports = (config: Config): void => { frameworks: ['jasmine', 'karma-typescript'], // list of files / patterns to load in the browser - files: ['./__karma__/jest.ts', 'packages/*/src/**/*.ts'], + files: ['./__karma__/jest.ts', './test-utils/*.ts', 'packages/*/src/**/*.ts'], // list of files / patterns to exclude exclude: ['packages/cli/**', '**/*.jest.test.ts'], @@ -24,6 +25,7 @@ module.exports = (config: Config): void => { preprocessors: { 'packages/*/src/**/*.ts': ['karma-typescript'], './__karma__/**/*.ts': ['karma-typescript'], + './test-utils/*.ts': ['karma-typescript'], }, // @ts-expect-error: non-standard - karmaTypeScriptConfig @@ -35,6 +37,7 @@ module.exports = (config: Config): void => { resolve: { alias: { '@stoplight/spectral-test-utils': require.resolve('./test-utils/browser/index.js'), + '@stoplight/spectral-test-utils/matchers': path.join(__dirname, './test-utils/matchers.ts'), nimma: require.resolve('./node_modules/nimma/dist/legacy/cjs/index.js'), 'nimma/fallbacks': require.resolve('./node_modules/nimma/dist/legacy/cjs/fallbacks/index.js'), 'nimma/legacy': require.resolve('./node_modules/nimma/dist/legacy/cjs/index.js'), diff --git a/package.json b/package.json index 6ad47cc47..54e38d9b5 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/*/CHANGELOG.md docs/**/*.md README.md", + "lint.prettier": "prettier --ignore-path .eslintignore --ignore-unknown --check packages/core/src/ruleset/meta/*.json packages/*/CHANGELOG.md 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", @@ -83,7 +83,6 @@ "jest": "^27.4.3", "jest-mock": "^27.4.2", "jest-when": "^3.4.2", - "json-schema": "^0.4.0", "karma": "^6.1.1", "karma-chrome-launcher": "^3.1.0", "karma-jasmine": "^3.3.1", @@ -109,7 +108,7 @@ "README.md": [ "prettier --write" ], - "packages/core/src/meta/*.json": [ + "packages/core/src/ruleset/meta/*.json": [ "prettier --ignore-path .eslintignore --write" ] }, diff --git a/packages/cli/src/services/__tests__/linter.test.ts b/packages/cli/src/services/__tests__/linter.test.ts index 4ab846dea..55ed42d00 100644 --- a/packages/cli/src/services/__tests__/linter.test.ts +++ b/packages/cli/src/services/__tests__/linter.test.ts @@ -1,3 +1,5 @@ +import '@stoplight/spectral-test-utils/matchers'; + import { join, resolve } from '@stoplight/path'; import nock from 'nock'; import * as yargs from 'yargs'; @@ -5,6 +7,8 @@ import lintCommand from '../../commands/lint'; import { lint } from '../linter'; import { DiagnosticSeverity } from '@stoplight/types'; import { RulesetValidationError } from '@stoplight/spectral-core'; +import '@stoplight/spectral-test-utils/matchers'; +import AggregateError = require('es-aggregate-error'); jest.mock('../output'); @@ -204,8 +208,32 @@ describe('Linter service', () => { }); it('fails trying to extend an invalid relative ruleset', () => { - return expect(run(`lint ${validCustomOas3SpecPath} -r ${invalidNestedRulesetPath}`)).rejects.toThrowError( - RulesetValidationError, + return expect( + run(`lint ${validCustomOas3SpecPath} -r ${invalidNestedRulesetPath}`), + ).rejects.toThrowAggregateError( + new AggregateError([ + new RulesetValidationError('must be equal to one of the allowed values', [ + 'rules', + 'rule-with-invalid-enum', + ]), + new RulesetValidationError('the rule must have at least "given" and "then" properties', [ + 'rules', + 'rule-without-given-nor-them', + ]), + new RulesetValidationError('allowed types are "style" and "validation"', [ + 'rules', + 'rule-with-invalid-enum', + 'type', + ]), + new RulesetValidationError('must be equal to one of the allowed values', [ + 'rules', + 'rule-without-given-nor-them', + ]), + new RulesetValidationError( + 'the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"', + ['rules', 'rule-with-invalid-enum', 'severity'], + ), + ]), ); }); }); @@ -218,8 +246,30 @@ describe('Linter service', () => { }); it('outputs "invalid ruleset" error', () => { - return expect(run(`lint ${validOas3SpecPath} -r ${invalidRulesetPath}`)).rejects.toThrowError( - RulesetValidationError, + return expect(run(`lint ${validOas3SpecPath} -r ${invalidRulesetPath}`)).rejects.toThrowAggregateError( + new AggregateError([ + new RulesetValidationError('must be equal to one of the allowed values', [ + 'rules', + 'rule-with-invalid-enum', + ]), + new RulesetValidationError('the rule must have at least "given" and "then" properties', [ + 'rules', + 'rule-without-given-nor-them', + ]), + new RulesetValidationError('allowed types are "style" and "validation"', [ + 'rules', + 'rule-with-invalid-enum', + 'type', + ]), + new RulesetValidationError('must be equal to one of the allowed values', [ + 'rules', + 'rule-without-given-nor-them', + ]), + new RulesetValidationError( + 'the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"', + ['rules', 'rule-with-invalid-enum', 'severity'], + ), + ]), ); }); diff --git a/packages/core/package.json b/packages/core/package.json index c1d8a63ca..eb71a65ef 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -29,12 +29,14 @@ "@stoplight/spectral-parsers": "^1.0.0", "@stoplight/spectral-ref-resolver": "^1.0.0", "@stoplight/spectral-runtime": "^1.0.0", - "@stoplight/types": "12.3.0", + "@stoplight/types": "12.5.0", + "@types/es-aggregate-error": "^1.0.2", + "@types/json-schema": "^7.0.7", "ajv": "^8.6.0", "ajv-errors": "~3.0.0", "ajv-formats": "~2.1.0", "blueimp-md5": "2.18.0", - "json-schema": "0.4.0", + "es-aggregate-error": "^1.0.7", "jsonpath-plus": "6.0.1", "lodash": "~4.17.21", "lodash.topath": "^4.5.2", @@ -48,7 +50,6 @@ "@stoplight/spectral-functions": "*", "@stoplight/spectral-parsers": "*", "@stoplight/yaml": "^4.2.2", - "@types/json-schema": "^7.0.7", "@types/minimatch": "^3.0.5", "@types/treeify": "^1.0.0", "nock": "^13.1.0", diff --git a/packages/core/src/ruleset/__tests__/__fixtures__/foo-extends-bar-ruleset.json b/packages/core/src/ruleset/__tests__/__fixtures__/foo-extends-bar-ruleset.json deleted file mode 100644 index 0f25ca5a2..000000000 --- a/packages/core/src/ruleset/__tests__/__fixtures__/foo-extends-bar-ruleset.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": ["./bar-extends-foo-ruleset.json"], - "rules": { - "foo-rule": { - "message": "Foo is falsy", - "given": "$.foo", - "then": { - "function": "falsy" - } - } - } -} diff --git a/packages/core/src/ruleset/__tests__/__fixtures__/foo-ruleset.json b/packages/core/src/ruleset/__tests__/__fixtures__/foo-ruleset.json deleted file mode 100644 index f483ef52d..000000000 --- a/packages/core/src/ruleset/__tests__/__fixtures__/foo-ruleset.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "functions": [ - "foo.cjs" - ], - "rules": { - "foo-rule": { - "message": "should be OK", - "given": "$.info", - "then": { - "function": "foo.cjs" - } - } - } -} diff --git a/packages/core/src/ruleset/__tests__/__fixtures__/invalid-ruleset.json b/packages/core/src/ruleset/__tests__/__fixtures__/invalid-ruleset.json deleted file mode 100644 index c885556db..000000000 --- a/packages/core/src/ruleset/__tests__/__fixtures__/invalid-ruleset.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "rules": { - "no-given-no-then": { - "message": "deliberetely invalid" - }, - "valid-rule": { - "message": "should be OK", - "given": "$.info", - "then": { - "function": "truthy" - } - }, - "rule-with-invalid-enum": { - "given": "$.info", - "then": { - "function": "truthy" - }, - "severity": "must not be a string", - "type": "some bs type value" - } - } -} diff --git a/packages/core/src/ruleset/__tests__/__fixtures__/overrides/formats.ts b/packages/core/src/ruleset/__tests__/__fixtures__/overrides/formats.ts index 1e5805cdb..fd6460068 100644 --- a/packages/core/src/ruleset/__tests__/__fixtures__/overrides/formats.ts +++ b/packages/core/src/ruleset/__tests__/__fixtures__/overrides/formats.ts @@ -22,7 +22,9 @@ const ruleset: RulesetDefinition = { then: { function: schema, functionOptions: { - type: 'number', + schema: { + type: 'number', + }, }, }, }, @@ -39,7 +41,9 @@ const ruleset: RulesetDefinition = { then: { function: schema, functionOptions: { - type: 'boolean', + schema: { + type: 'boolean', + }, }, }, }, diff --git a/packages/core/src/ruleset/__tests__/__fixtures__/ruleset-with-merge-keys.yaml b/packages/core/src/ruleset/__tests__/__fixtures__/ruleset-with-merge-keys.yaml deleted file mode 100644 index f10f02f51..000000000 --- a/packages/core/src/ruleset/__tests__/__fixtures__/ruleset-with-merge-keys.yaml +++ /dev/null @@ -1,20 +0,0 @@ -rules: - no-x-headers-request: &no-x-headers - description: "All 'HTTP' headers SHOULD NOT include 'X-' headers (https://tools.ietf.org/html/rfc6648)." - severity: warn - given: - - "$..parameters[?(@.in == 'header')].name" - message: |- - HTTP header '{{value}}' SHOULD NOT include 'X-' prefix in {{path}} - recommended: true - type: style - then: - function: pattern - functionOptions: - notMatch: "/^[xX]-/" - no-x-headers-response: - <<: *no-x-headers - given: - - $.[responses][*].headers.*~ - message: |- - HTTP header '{{value}}' SHOULD NOT include 'X-' prefix in {{path}} diff --git a/packages/core/src/ruleset/__tests__/__fixtures__/ruleset-with-missing-functions.json b/packages/core/src/ruleset/__tests__/__fixtures__/ruleset-with-missing-functions.json deleted file mode 100644 index 3573d2b49..000000000 --- a/packages/core/src/ruleset/__tests__/__fixtures__/ruleset-with-missing-functions.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rules": {}, - "functions": ["boo"] -} diff --git a/packages/core/src/ruleset/__tests__/__fixtures__/valid-flat-ruleset-2.json b/packages/core/src/ruleset/__tests__/__fixtures__/valid-flat-ruleset-2.json deleted file mode 100644 index 73b58dfbf..000000000 --- a/packages/core/src/ruleset/__tests__/__fixtures__/valid-flat-ruleset-2.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "rules": { - "valid-rule-2": { - "given": "$.info", - "then": { - "function": "truthy" - } - } - } -} diff --git a/packages/core/src/ruleset/__tests__/__fixtures__/valid-flat-ruleset.json b/packages/core/src/ruleset/__tests__/__fixtures__/valid-flat-ruleset.json deleted file mode 100644 index 21b4936bc..000000000 --- a/packages/core/src/ruleset/__tests__/__fixtures__/valid-flat-ruleset.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "rules": { - "valid-rule": { - "given": "$.info", - "then": { - "function": "truthy" - } - } - } -} diff --git a/packages/core/src/ruleset/__tests__/ruleset.test.ts b/packages/core/src/ruleset/__tests__/ruleset.test.ts index a0bbdb63b..99da1666b 100644 --- a/packages/core/src/ruleset/__tests__/ruleset.test.ts +++ b/packages/core/src/ruleset/__tests__/ruleset.test.ts @@ -6,10 +6,10 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { Ruleset } from '../ruleset'; import { RulesetDefinition } from '../types'; import { print } from './__helpers__/print'; -import { RulesetValidationError } from '../validation'; +import { RulesetValidationError } from '../validation/index'; import { isPlainObject } from '@stoplight/json'; import { Format } from '../format'; -import { JSONSchema4, JSONSchema6, JSONSchema7 } from 'json-schema'; +import type { JSONSchema4, JSONSchema6, JSONSchema7 } from 'json-schema'; import { FormatsSet } from '../utils/formatsSet'; async function loadRuleset(mod: Promise<{ default: RulesetDefinition }>, source?: string): Promise { @@ -247,7 +247,7 @@ describe('Ruleset', () => { describe('error handling', () => { it('given empty ruleset, should throw a user friendly error', () => { expect(() => new Ruleset({})).toThrowError( - new RulesetValidationError('Ruleset must have rules or extends or overrides defined'), + new RulesetValidationError('Ruleset must have rules or extends or overrides defined', []), ); }); }); diff --git a/packages/core/src/ruleset/rulesetFunction.ts b/packages/core/src/ruleset/function.ts similarity index 59% rename from packages/core/src/ruleset/rulesetFunction.ts rename to packages/core/src/ruleset/function.ts index 6741a0dce..2a680ab29 100644 --- a/packages/core/src/ruleset/rulesetFunction.ts +++ b/packages/core/src/ruleset/function.ts @@ -8,56 +8,58 @@ import type { JSONSchema7 } from 'json-schema'; import { printPath, PrintStyle, printValue } from '@stoplight/spectral-runtime'; -import { RulesetValidationError } from './validation'; +import { RulesetValidationError } from './validation/index'; import { IFunctionResult, JSONSchema, RulesetFunction, RulesetFunctionWithValidator } from '../types'; +import { isObject } from 'lodash'; +import AggregateError = require('es-aggregate-error'); const ajv = new Ajv({ allErrors: true, allowUnionTypes: true, strict: true, keywords: ['x-internal'] }); ajvErrors(ajv); addFormats(ajv); export class RulesetFunctionValidationError extends RulesetValidationError { - constructor(fn: string, errors: ErrorObject[]) { - const messages = errors.map(error => { - switch (error.keyword) { - case 'type': { - const path = printPath(error.instancePath.slice(1).split('/'), PrintStyle.Dot); - const values = Array.isArray(error.params.type) ? error.params.type.join(', ') : String(error.params.type); - - return `"${fn}" function and its "${path}" option accepts only the following types: ${values}`; - } - - case 'required': { - const missingProperty = (error as RequiredError).params.missingProperty; - const missingPropertyPath = - error.instancePath === '' - ? missingProperty - : printPath([...error.instancePath.slice(1).split('/'), missingProperty], PrintStyle.Dot); - - return `"${fn}" function is missing "${missingPropertyPath}" option`; - } - - case 'additionalProperties': { - const additionalProperty = (error as AdditionalPropertiesError).params.additionalProperty; - const additionalPropertyPath = - error.instancePath === '' - ? additionalProperty - : printPath([...error.instancePath.slice(1).split('/'), additionalProperty], PrintStyle.Dot); - - return `"${fn}" function does not support "${additionalPropertyPath}" option`; - } - - case 'enum': { - const path = printPath(error.instancePath.slice(1).split('/'), PrintStyle.Dot); - const values = (error as EnumError).params.allowedValues.map(printValue).join(', '); - - return `"${fn}" function and its "${path}" option accepts only the following values: ${values}`; - } - default: - return error.message; + constructor(fn: string, error: ErrorObject) { + super(RulesetFunctionValidationError.printMessage(fn, error), error.instancePath.slice(1).split('/')); + } + + private static printMessage(fn: string, error: ErrorObject): string { + switch (error.keyword) { + case 'type': { + const path = printPath(error.instancePath.slice(1).split('/'), PrintStyle.Dot); + const values = Array.isArray(error.params.type) ? error.params.type.join(', ') : String(error.params.type); + + return `"${fn}" function and its "${path}" option accepts only the following types: ${values}`; + } + + case 'required': { + const missingProperty = (error as RequiredError).params.missingProperty; + const missingPropertyPath = + error.instancePath === '' + ? missingProperty + : printPath([...error.instancePath.slice(1).split('/'), missingProperty], PrintStyle.Dot); + + return `"${fn}" function is missing "${missingPropertyPath}" option`; } - }); - super(messages.join('\n')); + case 'additionalProperties': { + const additionalProperty = (error as AdditionalPropertiesError).params.additionalProperty; + const additionalPropertyPath = + error.instancePath === '' + ? additionalProperty + : printPath([...error.instancePath.slice(1).split('/'), additionalProperty], PrintStyle.Dot); + + return `"${fn}" function does not support "${additionalPropertyPath}" option`; + } + + case 'enum': { + const path = printPath(error.instancePath.slice(1).split('/'), PrintStyle.Dot); + const values = (error as EnumError).params.allowedValues.map(printValue).join(', '); + + return `"${fn}" function and its "${path}" option accepts only the following values: ${values}`; + } + default: + return error.message ?? 'unknown error'; + } } } @@ -138,25 +140,27 @@ export function createRulesetFunction( Reflect.defineProperty(wrappedFn, 'name', { value: fn.name }); - const validOpts = new Set(); + const validOpts = new WeakSet(); wrappedFn.validator = function (o: unknown): asserts o is O { - if (validOpts.has(o)) return; // I don't like this. + if (isObject(o) && validOpts.has(o)) return; // I don't like this. if (validateOptions(o)) { - validOpts.add(o); + if (isObject(o)) validOpts.add(o); return; } if (options === null) { - throw new TypeError(`"${fn.name || ''}" function does not accept any options`); + throw new RulesetValidationError(`"${fn.name || ''}" function does not accept any options`, []); } else if ( 'errors' in validateOptions && Array.isArray(validateOptions.errors) && validateOptions.errors.length > 0 ) { - throw new RulesetFunctionValidationError(fn.name || '', validateOptions.errors); + throw new AggregateError( + validateOptions.errors.map(error => new RulesetFunctionValidationError(fn.name || '', error)), + ); } else { - throw new Error(`"functionOptions" of "${fn.name || ''}" function must be valid`); + throw new RulesetValidationError(`"functionOptions" of "${fn.name || ''}" function must be valid`, []); } }; diff --git a/packages/core/src/ruleset/index.ts b/packages/core/src/ruleset/index.ts index e3ac2fb6e..87eff1de0 100644 --- a/packages/core/src/ruleset/index.ts +++ b/packages/core/src/ruleset/index.ts @@ -1,6 +1,6 @@ -export { assertValidRuleset, RulesetValidationError } from './validation'; +export { assertValidRuleset, RulesetValidationError } from './validation/index'; export { getDiagnosticSeverity } from './utils'; -export { createRulesetFunction, SchemaDefinition as RulesetFunctionSchemaDefinition } from './rulesetFunction'; +export { createRulesetFunction, SchemaDefinition as RulesetFunctionSchemaDefinition } from './function'; export { Format } from './format'; export { RulesetDefinition, RuleDefinition, ParserOptions, HumanReadableDiagnosticSeverity } from './types'; export { Ruleset } from './ruleset'; diff --git a/packages/core/src/ruleset/mergers/rules.ts b/packages/core/src/ruleset/mergers/rules.ts index 04396b5b0..20b622866 100644 --- a/packages/core/src/ruleset/mergers/rules.ts +++ b/packages/core/src/ruleset/mergers/rules.ts @@ -1,5 +1,5 @@ import { Optional } from '@stoplight/types'; -import { assertValidRule } from '../validation'; +import { assertValidRule } from '../validation/index'; import { Rule } from '../rule'; import type { Ruleset } from '../ruleset'; import { FileRuleDefinition } from '../types'; @@ -41,7 +41,7 @@ export function mergeRule( if (existingRule !== void 0) { Object.assign(existingRule, rule, { owner: existingRule.owner }); } else { - assertValidRule(rule); + assertValidRule(rule, name); return new Rule(name, rule, ruleset); } diff --git a/packages/core/src/ruleset/meta/js-extensions.json b/packages/core/src/ruleset/meta/js-extensions.json new file mode 100644 index 000000000..e6c717d8c --- /dev/null +++ b/packages/core/src/ruleset/meta/js-extensions.json @@ -0,0 +1,64 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "@stoplight/spectral-core/meta/extensions", + "$defs": { + "Extends": { + "$anchor": "extends", + "oneOf": [ + { + "$id": "ruleset", + "$ref": "ruleset.schema#", + "errorMessage": "must be a valid ruleset" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "ruleset" + }, + { + "type": "array", + "minItems": 2, + "additionalItems": false, + "items": [ + { + "$ref": "ruleset" + }, + { + "type": "string", + "enum": ["off", "recommended", "all"], + "errorMessage": "allowed types are \"off\", \"recommended\" and \"all\"" + } + ] + } + ] + } + } + ], + "errorMessage": "must be a valid ruleset" + }, + "Format": { + "$anchor": "format", + "x-spectral-runtime": "format", + "errorMessage": "must be a valid format" + }, + "Function": { + "$anchor": "function", + "x-spectral-runtime": "ruleset-function", + "type": "object", + "properties": { + "function": true + }, + "required": ["function"] + }, + "Functions": { + "$anchor": "functions", + "not": {} + }, + "FunctionsDir": { + "$anchor": "functionsDir", + "not": {} + } + } +} diff --git a/packages/core/src/ruleset/meta/json-extensions.json b/packages/core/src/ruleset/meta/json-extensions.json new file mode 100644 index 000000000..6729e6380 --- /dev/null +++ b/packages/core/src/ruleset/meta/json-extensions.json @@ -0,0 +1,78 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "@stoplight/spectral-core/meta/extensions", + "$defs": { + "Extends": { + "$anchor": "extends", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 2, + "additionalItems": false, + "items": [ + { + "type": "string" + }, + { + "enum": ["all", "recommended", "off"] + } + ] + } + ] + } + } + ] + }, + "Format": { + "$anchor": "format", + "enum": [ + "oas2", + "oas3", + "oas3.0", + "oas3.1", + "asyncapi2", + "json-schema", + "json-schema-loose", + "json-schema-draft4", + "json-schema-draft6", + "json-schema-draft7", + "json-schema-draft-2019-09", + "json-schema-2019-09", + "json-schema-draft-2020-12", + "json-schema-2020-12" + ], + "errorMessage": "must be a valid format" + }, + "Functions": { + "$anchor": "functions", + "type": "array", + "items": { + "type": "string" + } + }, + "FunctionsDir": { + "$anchor": "functionsDir", + "type": "string" + }, + "Function": { + "$anchor": "function", + "type": "object", + "properties": { + "function": { + "type": "string" + } + }, + "required": ["function"] + } + } +} diff --git a/packages/core/src/meta/rule.schema.json b/packages/core/src/ruleset/meta/rule.schema.json similarity index 94% rename from packages/core/src/meta/rule.schema.json rename to packages/core/src/ruleset/meta/rule.schema.json index d49fb758b..4372cacff 100644 --- a/packages/core/src/meta/rule.schema.json +++ b/packages/core/src/ruleset/meta/rule.schema.json @@ -13,9 +13,7 @@ } }, { - "type": "object", - "spectral-runtime": "ruleset-function", - "required": ["function"] + "$ref": "extensions#function" } ] }, diff --git a/packages/core/src/meta/ruleset.schema.json b/packages/core/src/ruleset/meta/ruleset.schema.json similarity index 85% rename from packages/core/src/meta/ruleset.schema.json rename to packages/core/src/ruleset/meta/ruleset.schema.json index 2ae5dae5e..06ff3eef4 100644 --- a/packages/core/src/meta/ruleset.schema.json +++ b/packages/core/src/ruleset/meta/ruleset.schema.json @@ -9,49 +9,26 @@ "format": "url", "errorMessage": "must be a valid URL" }, - "description": { "type": "string" }, + "description": { + "type": "string" + }, "rules": { "type": "object", "additionalProperties": { "$ref": "rule.schema#" } }, + "functions": { + "$ref": "extensions#functions" + }, + "functionsDir": { + "$ref": "extensions#functionsDir" + }, "formats": { "$ref": "shared#formats" }, "extends": { - "oneOf": [ - { - "$ref": "#", - "errorMessage": "must be a valid ruleset" - }, - { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/properties/extends/oneOf/0" - }, - { - "type": "array", - "minItems": 2, - "additionalItems": false, - "items": [ - { - "$ref": "#" - }, - { - "type": "string", - "enum": ["off", "recommended", "all"], - "errorMessage": "allowed types are \"off\", \"recommended\" and \"all\"" - } - ] - } - ] - } - } - ], - "errorMessage": "must be a valid ruleset" + "$ref": "extensions#extends" }, "parserOptions": { "type": "object", diff --git a/packages/core/src/meta/shared.json b/packages/core/src/ruleset/meta/shared.json similarity index 89% rename from packages/core/src/meta/shared.json rename to packages/core/src/ruleset/meta/shared.json index 9cacc71ef..00ce7e2dd 100644 --- a/packages/core/src/meta/shared.json +++ b/packages/core/src/ruleset/meta/shared.json @@ -6,15 +6,10 @@ "$anchor": "formats", "type": "array", "items": { - "$ref": "#format" + "$ref": "extensions#format" }, "errorMessage": "must be an array of formats" }, - "Format": { - "$anchor": "format", - "spectral-runtime": "spectral-format", - "errorMessage": "must be a valid format" - }, "DiagnosticSeverity": { "enum": [-1, 0, 1, 2, 3] }, diff --git a/packages/core/src/ruleset/ruleset.ts b/packages/core/src/ruleset/ruleset.ts index 0dbf80c0b..cc123c490 100644 --- a/packages/core/src/ruleset/ruleset.ts +++ b/packages/core/src/ruleset/ruleset.ts @@ -8,7 +8,7 @@ import { RulesetDefinition, RulesetOverridesDefinition, } from './types'; -import { assertValidRuleset } from './validation'; +import { assertValidRuleset } from './validation/index'; import { mergeRule } from './mergers/rules'; import { DEFAULT_PARSER_OPTIONS, getDiagnosticSeverity } from '..'; import { mergeRulesets } from './mergers/rulesets'; @@ -41,15 +41,13 @@ export type StringifiedRuleset = { export class Ruleset { public readonly id = SEED++; - - protected readonly extends: Ruleset[] | null; public readonly formats = new FormatsSet(); public readonly overrides: RulesetOverridesDefinition | null; public readonly aliases: RulesetAliasesDefinition | null; public readonly hasComplexAliases: boolean; public readonly rules: Record; public readonly definition: RulesetDefinition; - + protected readonly extends: Ruleset[] | null; readonly #context: RulesetContext & { severity: FileRulesetSeverityDefinition }; constructor(readonly maybeDefinition: unknown, context?: RulesetContext) { @@ -155,6 +153,14 @@ export class Ruleset { return this.#context.source ?? null; } + public get parserOptions(): ParserOptions { + return { ...DEFAULT_PARSER_OPTIONS, ...this.definition.parserOptions }; + } + + public static isDefaultRulesetFile(uri: string): boolean { + return DEFAULT_RULESET_FILE.test(uri); + } + public fromSource(source: string | null): Ruleset { if (this.overrides === null) { return this; @@ -259,6 +265,22 @@ export class Ruleset { return ruleset; } + public toJSON(): Omit & { + extends: Ruleset['extends']; + rules: Ruleset['rules']; + } { + return { + id: this.id, + extends: this.extends, + source: this.source, + aliases: this.aliases, + formats: this.formats.size === 0 ? null : this.formats, + rules: this.rules, + overrides: this.overrides, + parserOptions: this.parserOptions, + }; + } + #getRules(): Record { const rules: Record = {}; @@ -300,28 +322,4 @@ export class Ruleset { return rules; } - - public get parserOptions(): ParserOptions { - return { ...DEFAULT_PARSER_OPTIONS, ...this.definition.parserOptions }; - } - - public static isDefaultRulesetFile(uri: string): boolean { - return DEFAULT_RULESET_FILE.test(uri); - } - - public toJSON(): Omit & { - extends: Ruleset['extends']; - rules: Ruleset['rules']; - } { - return { - id: this.id, - extends: this.extends, - source: this.source, - aliases: this.aliases, - formats: this.formats.size === 0 ? null : this.formats, - rules: this.rules, - overrides: this.overrides, - parserOptions: this.parserOptions, - }; - } } diff --git a/packages/core/src/ruleset/validation.ts b/packages/core/src/ruleset/validation.ts deleted file mode 100644 index 8df74f589..000000000 --- a/packages/core/src/ruleset/validation.ts +++ /dev/null @@ -1,129 +0,0 @@ -import Ajv, { _, ErrorObject } from 'ajv'; -import addFormats from 'ajv-formats'; -import addErrors from 'ajv-errors'; -import { isPlainObject } from '@stoplight/json'; -import { printPath, PrintStyle } from '@stoplight/spectral-runtime'; - -import * as ruleSchema from '../meta/rule.schema.json'; -import * as rulesetSchema from '../meta/ruleset.schema.json'; -import * as shared from '../meta/shared.json'; -import type { FileRuleDefinition, RuleDefinition, RulesetDefinition } from './types'; - -const message = _`'spectral-message'`; - -const ajv = new Ajv({ allErrors: true, strict: true, strictRequired: false, keywords: ['$anchor', 'x-internal'] }); -addFormats(ajv); -addErrors(ajv); -ajv.addKeyword({ - keyword: 'spectral-runtime', - schemaType: 'string', - error: { - message(ctx) { - return _`${ctx.data}[Symbol.for(${message})]`; - }, - }, - code(cxt) { - const { data } = cxt; - - switch (cxt.schema as unknown) { - case 'spectral-format': - cxt.fail(_`typeof ${data} !== "function"`); - break; - case 'spectral-function': - cxt.pass(_`typeof ${data}.function === "function"`); - cxt.pass( - _`(() => { try { ${data}.function.validator && ${data}.function.validator('functionOptions' in ${data} ? ${data} : null); } catch (e) { ${data}[${message}] = e.message } })()`, - ); - break; - } - }, -}); - -const validate = ajv.addSchema(ruleSchema).addSchema(shared).compile(rulesetSchema); - -export class RulesetValidationError extends Error { - constructor(public readonly message: string) { - super(message); - } -} - -const RULE_INSTANCE_PATH = /^\/rules\/[^/]+/; -const GENERIC_INSTANCE_PATH = /^\/(?:aliases|extends|overrides(?:\/\d+\/extends)?)/; - -class RulesetAjvValidationError extends RulesetValidationError { - constructor(public ruleset: Record, public errors: ErrorObject[]) { - super(RulesetAjvValidationError.serializeAjvErrors(ruleset, errors)); - } - - public static serializeAjvErrors(ruleset: Record, errors: ErrorObject[]): string { - const sortedErrors = [...errors] - .sort((errorA, errorB) => { - const diff = errorA.instancePath.length - errorB.instancePath.length; - return diff === 0 ? (errorA.keyword === 'errorMessage' && errorB.keyword !== 'errorMessage' ? -1 : 0) : diff; - }) - .filter((error, i, sortedErrors) => i === 0 || sortedErrors[i - 1].instancePath !== error.instancePath); - - const filteredErrors: ErrorObject[] = []; - - l: for (let i = 0; i < sortedErrors.length; i++) { - const error = sortedErrors[i]; - const prevError = i === 0 ? null : sortedErrors[i - 1]; - - if (GENERIC_INSTANCE_PATH.test(error.instancePath)) { - let x = 1; - while (i + x < sortedErrors.length) { - if ( - sortedErrors[i + x].instancePath.startsWith(error.instancePath) || - !GENERIC_INSTANCE_PATH.test(sortedErrors[i + x].instancePath) - ) { - continue l; - } - - x++; - } - } else if (prevError === null) { - filteredErrors.push(error); - continue; - } else { - const match = RULE_INSTANCE_PATH.exec(error.instancePath); - - if (match !== null && match[0] !== match.input && match[0] === prevError.instancePath) { - filteredErrors.pop(); - } - } - - filteredErrors.push(error); - } - - return filteredErrors - .map( - ({ message, instancePath }) => - `Error at ${printPath(instancePath.slice(1).split('/'), PrintStyle.Pointer)}: ${message ?? ''}`, - ) - .join('\n'); - } -} - -export function assertValidRuleset(ruleset: unknown): asserts ruleset is RulesetDefinition { - if (!isPlainObject(ruleset)) { - throw new Error('Provided ruleset is not an object'); - } - - if (!('rules' in ruleset) && !('extends' in ruleset) && !('overrides' in ruleset)) { - throw new RulesetValidationError('Ruleset must have rules or extends or overrides defined'); - } - - if (!validate(ruleset)) { - throw new RulesetAjvValidationError(ruleset, validate.errors ?? []); - } -} - -export function isValidRule(rule: FileRuleDefinition): rule is RuleDefinition { - return typeof rule === 'object' && rule !== null && !Array.isArray(rule) && ('given' in rule || 'then' in rule); -} - -export function assertValidRule(rule: FileRuleDefinition): asserts rule is RuleDefinition { - if (!isValidRule(rule)) { - throw new TypeError('Invalid rule'); - } -} diff --git a/packages/core/src/ruleset/validation/__tests__/__fixtures__/invalid-ruleset.ts b/packages/core/src/ruleset/validation/__tests__/__fixtures__/invalid-ruleset.ts new file mode 100644 index 000000000..a041af340 --- /dev/null +++ b/packages/core/src/ruleset/validation/__tests__/__fixtures__/invalid-ruleset.ts @@ -0,0 +1,24 @@ +import { truthy } from '@stoplight/spectral-functions'; + +export default { + rules: { + 'no-given-no-then': { + message: 'deliberately invalid', + }, + 'valid-rule': { + message: 'should be OK', + given: '$.info', + then: { + function: truthy, + }, + }, + 'rule-with-invalid-enum': { + given: '$.info', + then: { + function: truthy, + }, + severity: 'must not be a string', + type: 'some bs type value', + }, + }, +}; diff --git a/packages/core/src/ruleset/validation/__tests__/__fixtures__/valid-flat-ruleset.ts b/packages/core/src/ruleset/validation/__tests__/__fixtures__/valid-flat-ruleset.ts new file mode 100644 index 000000000..94f809ceb --- /dev/null +++ b/packages/core/src/ruleset/validation/__tests__/__fixtures__/valid-flat-ruleset.ts @@ -0,0 +1,12 @@ +import { truthy } from '@stoplight/spectral-functions'; + +export default { + rules: { + 'valid-rule': { + given: '$.info', + then: { + function: truthy, + }, + }, + }, +}; diff --git a/packages/core/src/ruleset/__tests__/validation.test.ts b/packages/core/src/ruleset/validation/__tests__/validation.test.ts similarity index 62% rename from packages/core/src/ruleset/__tests__/validation.test.ts rename to packages/core/src/ruleset/validation/__tests__/validation.test.ts index b4bee4cf5..58f688c05 100644 --- a/packages/core/src/ruleset/__tests__/validation.test.ts +++ b/packages/core/src/ruleset/validation/__tests__/validation.test.ts @@ -1,13 +1,20 @@ -import { schema, truthy } from '@stoplight/spectral-functions'; -import { Format } from '../format'; -import { assertValidRuleset, RulesetValidationError } from '../validation'; -import { RulesetDefinition, RulesetOverridesDefinition } from '../types'; -const invalidRuleset = require('./__fixtures__/invalid-ruleset.json'); -const validRuleset = require('./__fixtures__/valid-flat-ruleset.json'); +import '@stoplight/spectral-test-utils/matchers'; + +import { assertValidRuleset, RulesetValidationError } from '../index'; +import AggregateError = require('es-aggregate-error'); +import invalidRuleset from './__fixtures__/invalid-ruleset'; +import validRuleset from './__fixtures__/valid-flat-ruleset'; + +import type { Format } from '../../format'; +import { RulesetDefinition, RulesetOverridesDefinition } from '../../types'; const formatA: Format = () => false; const formatB: Format = () => false; +function truthy() { + // no-op +} + describe('Ruleset Validation', () => { it('given primitive type, throws', () => { expect(assertValidRuleset.bind(null, null)).toThrow('Provided ruleset is not an object'); @@ -31,10 +38,18 @@ describe('Ruleset Validation', () => { }); it('given invalid ruleset, throws', () => { - expect(assertValidRuleset.bind(null, invalidRuleset)).toThrow( - new RulesetValidationError(`Error at #/rules/no-given-no-then: the rule must have at least "given" and "then" properties -Error at #/rules/rule-with-invalid-enum/type: allowed types are "style" and "validation" -Error at #/rules/rule-with-invalid-enum/severity: the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"`), + expect(assertValidRuleset.bind(null, invalidRuleset)).toThrowAggregateError( + new AggregateError([ + new RulesetValidationError('the rule must have at least "given" and "then" properties', [ + '/rules/no-given-no-then', + ]), + new RulesetValidationError('allowed types are "style" and "validation"', [ + '#/rules/rule-with-invalid-enum/type', + ]), + new RulesetValidationError('the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"', [ + '#/rules/rule-with-invalid-enum/severity', + ]), + ]), ); }); @@ -45,8 +60,8 @@ Error at #/rules/rule-with-invalid-enum/severity: the value has to be one of: 0, it.each([false, 2, null, 'foo', '12.foo.com'])( 'given invalid %s documentationUrl in a rule, throws', documentationUrl => { - expect(assertValidRuleset.bind(null, { documentationUrl, rules: {} })).toThrow( - new RulesetValidationError('Error at #/documentationUrl: must be a valid URL'), + expect(assertValidRuleset.bind(null, { documentationUrl, rules: {} })).toThrowAggregateError( + new AggregateError([new RulesetValidationError('must be a valid URL', ['documentationUrl'])]), ); expect( @@ -56,12 +71,14 @@ Error at #/rules/rule-with-invalid-enum/severity: the value has to be one of: 0, documentationUrl, given: '$', then: { - function: '', + function: truthy, }, }, }, }), - ).toThrow(new RulesetValidationError('Error at #/rules/rule/documentationUrl: must be a valid URL')); + ).toThrowAggregateError( + new AggregateError([new RulesetValidationError('must be a valid URL', ['rules', 'rule', 'documentationUrl'])]), + ); }, ); @@ -80,7 +97,7 @@ Error at #/rules/rule-with-invalid-enum/severity: the value has to be one of: 0, documentationUrl: 'https://stoplight.io/p/docs/gh/stoplightio/spectral/docs/reference/openapi-rules.md', given: '$', then: { - function: '', + function: truthy, }, }, }, @@ -88,21 +105,6 @@ Error at #/rules/rule-with-invalid-enum/severity: the value has to be one of: 0, ).not.toThrow(); }); - it.each([false, 2, null])('given invalid %s description in a ruleset, throws', description => { - expect(assertValidRuleset.bind(null, { description, rules: {} })).toThrow( - new RulesetValidationError('Error at #/description: must be string'), - ); - }); - - it('recognizes valid description in a ruleset', () => { - expect( - assertValidRuleset.bind(null, { - description: 'This is the ruleset description', - rules: {}, - }), - ).not.toThrow(); - }); - it.each(['error', 'warn', 'info', 'hint', 'off'])('recognizes human-readable %s severity', severity => { expect( assertValidRuleset.bind(null, { @@ -118,7 +120,7 @@ Error at #/rules/rule-with-invalid-enum/severity: the value has to be one of: 0, rule: { given: '$.info', then: { - function: 'truthy', + function: truthy, }, severity, }, @@ -144,35 +146,24 @@ Error at #/rules/rule-with-invalid-enum/severity: the value has to be one of: 0, ).not.toThrow(); }); - it('recognizes string extends syntax', () => { - expect( - assertValidRuleset.bind(null, { - rules: { - foo: { - given: '$', - then: { - function: schema, - functionOptions: {}, - }, - }, - }, - }), - ).not.toThrow(); - }); - - it.each<[unknown, string]>([ - [[[{ rules: {} }, 'test']], `Error at #/extends/0/1: allowed types are "off", "recommended" and "all"`], + it.each<[unknown, RulesetValidationError[]]>([ + [ + [[{ rules: {} }, 'test']], + [new RulesetValidationError('allowed types are "off", "recommended" and "all"', ['extends', '0', '1'])], + ], [ [[{ rules: {} }, 'test'], 'foo'], - `Error at #/extends/1: must be a valid ruleset -Error at #/extends/0/1: allowed types are "off", "recommended" and "all"`, + [ + new RulesetValidationError('must be a valid ruleset', ['extends', '1']), + new RulesetValidationError('allowed types are "off", "recommended" and "all"', ['extends', '0', '1']), + ], ], - ])('recognizes invalid array-ish extends syntax %p', (_extends, message) => { + ])('recognizes invalid array-ish extends syntax %p', (_extends, errors) => { expect( assertValidRuleset.bind(null, { extends: _extends, }), - ).toThrow(new RulesetValidationError(message)); + ).toThrowAggregateError(new AggregateError(errors)); }); it('recognizes valid ruleset formats syntax', () => { @@ -187,18 +178,20 @@ Error at #/extends/0/1: allowed types are "off", "recommended" and "all"`, it.each([ [ [2, 'a'], - `Error at #/formats/0: must be a valid format -Error at #/formats/1: must be a valid format`, + new AggregateError([ + new RulesetValidationError('must be a valid format', ['formats', '0']), + new RulesetValidationError('must be a valid format', ['formats', '1']), + ]), ], - [2, 'Error at #/formats: must be an array of formats'], - [[''], 'Error at #/formats/0: must be a valid format'], + [2, new AggregateError([new RulesetValidationError('must be an array of formats', ['formats'])])], + [[''], new AggregateError([new RulesetValidationError('must be a valid format', ['formats', '0'])])], ])('recognizes invalid ruleset %p formats syntax', (formats, error) => { expect( assertValidRuleset.bind(null, { formats, rules: {}, }), - ).toThrow(new RulesetValidationError(error)); + ).toThrowAggregateError(error); }); it('recognizes valid rule formats syntax', () => { @@ -209,7 +202,7 @@ Error at #/formats/1: must be a valid format`, rule: { given: '$.info', then: { - function: 'truthy', + function: truthy, }, formats: [formatA], }, @@ -221,10 +214,12 @@ Error at #/formats/1: must be a valid format`, it.each([ [ [2, 'a'], - `Error at #/rules/rule/formats/0: must be a valid format -Error at #/rules/rule/formats/1: must be a valid format`, + new AggregateError([ + new RulesetValidationError('must be a valid format', ['rules', 'rule', 'formats', '0']), + new RulesetValidationError('must be a valid format', ['rules', 'rule', 'formats', '1']), + ]), ], - [2, 'Error at #/rules/rule/formats: must be an array of formats'], + [2, new AggregateError([new RulesetValidationError('must be an array of formats', ['rules', 'rule', 'formats'])])], ])('recognizes invalid rule %p formats syntax', (formats, error) => { expect( assertValidRuleset.bind(null, { @@ -232,13 +227,13 @@ Error at #/rules/rule/formats/1: must be a valid format`, rule: { given: '$.info', then: { - function: 'truthy', + function: truthy, }, formats, }, }, }), - ).toThrow(new RulesetValidationError(error)); + ).toThrowAggregateError(error); }); describe('overrides validation', () => { @@ -247,7 +242,7 @@ Error at #/rules/rule/formats/1: must be a valid format`, assertValidRuleset.bind(null, { overrides: null, }), - ).toThrow(new RulesetValidationError('Error at #/overrides: must be array')); + ).toThrowAggregateError(new AggregateError([new RulesetValidationError('must be array', ['overrides'])])); }); it('given an empty overrides, throws', () => { @@ -255,7 +250,7 @@ Error at #/rules/rule/formats/1: must be a valid format`, assertValidRuleset.bind(null, { overrides: [], }), - ).toThrow(new RulesetValidationError('Error at #/overrides: must not be empty')); + ).toThrowAggregateError(new AggregateError([new RulesetValidationError('must not be empty', ['overrides'])])); }); it('given an invalid pattern, throws', () => { @@ -263,10 +258,13 @@ Error at #/rules/rule/formats/1: must be a valid format`, assertValidRuleset.bind(null, { overrides: [2], }), - ).toThrow( - new RulesetValidationError( - 'Error at #/overrides/0: must be a override, i.e. { "files": ["v2/**/*.json"], "rules": {} }', - ), + ).toThrowAggregateError( + new AggregateError([ + new RulesetValidationError('must be a override, i.e. { "files": ["v2/**/*.json"], "rules": {} }', [ + 'overrides', + '0', + ]), + ]), ); }); @@ -275,16 +273,28 @@ Error at #/rules/rule/formats/1: must be a valid format`, rules: {}, }; - it.each<[Partial, string]>([ - [{ extends: [rulesetA] }, 'Error at #/overrides/0: must contain rules when JSON Pointers are defined'], - [{ formats: [formatB] }, 'Error at #/overrides/0: must contain rules when JSON Pointers are defined'], + it.each<[Partial, RulesetValidationError]>([ + [ + { extends: [rulesetA] }, + new RulesetValidationError('must contain rules when JSON Pointers are defined', ['overrides', '0']), + ], + [ + { formats: [formatB] }, + new RulesetValidationError('must contain rules when JSON Pointers are defined', ['overrides', '0']), + ], [ { rules: {}, formats: [formatB] }, - 'Error at #/overrides/0: must not override any other property than rules when JSON Pointers are defined', + new RulesetValidationError('must not override any other property than rules when JSON Pointers are defined', [ + 'overrides', + '0', + ]), ], [ { rules: {}, extends: [rulesetA] }, - 'Error at #/overrides/0: must not override any other property than rules when JSON Pointers are defined', + new RulesetValidationError('must not override any other property than rules when JSON Pointers are defined', [ + 'overrides', + '0', + ]), ], [ { @@ -297,7 +307,10 @@ Error at #/rules/rule/formats/1: must be a valid format`, }, }, }, - 'Error at #/overrides/0/rules/definition: the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"', + new RulesetValidationError( + 'the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"', + ['overrides', '0', 'rules', 'definition'], + ), ], ])('given an override containing a pointer and %p, throws', (ruleset, error) => { expect( @@ -309,7 +322,7 @@ Error at #/rules/rule/formats/1: must be a valid format`, }, ], }), - ).toThrow(new RulesetValidationError(error)); + ).toThrowAggregateError(new AggregateError([error])); }); it.each([ @@ -369,7 +382,7 @@ Error at #/rules/rule/formats/1: must be a valid format`, rules: {}, aliases: null, }), - ).toThrow(new RulesetValidationError('Error at #/aliases: must be object')); + ).toThrowAggregateError(new AggregateError([new RulesetValidationError('must be object', ['aliases'])])); }); it.each([null, 5])('recognizes %p as an invalid type of aliases', alias => { @@ -380,10 +393,13 @@ Error at #/rules/rule/formats/1: must be a valid format`, alias, }, }), - ).toThrow( - new RulesetValidationError( - 'Error at #/aliases/alias: must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset', - ), + ).toThrowAggregateError( + new AggregateError([ + new RulesetValidationError( + 'must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset', + ['aliases', 'alias'], + ), + ]), ); }); @@ -395,28 +411,46 @@ Error at #/rules/rule/formats/1: must be a valid format`, [key]: '$.foo', }, }), - ).toThrow( - new RulesetValidationError( - 'Error at #/aliases: to avoid confusion the name must match /^[A-Za-z][A-Za-z0-9_-]*$/ regular expression', - ), + ).toThrowAggregateError( + new AggregateError([ + new RulesetValidationError( + 'to avoid confusion the name must match /^[A-Za-z][A-Za-z0-9_-]*$/ regular expression', + ['aliases'], + ), + ]), ); }); - it.each<[unknown, string]>([ + it.each<[unknown, RulesetValidationError[]]>([ [ '', - 'Error at #/aliases/PathItem: must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset', + [ + new RulesetValidationError( + 'must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset', + ['aliases', 'PathItem'], + ), + ], ], [ 'foo', - 'Error at #/aliases/PathItem: must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset', + [ + new RulesetValidationError( + 'must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset', + ['aliases', 'PathItem'], + ), + ], ], - [[], 'Error at #/aliases/PathItem: must be a non-empty array of expressions'], + [[], [new RulesetValidationError('must be a non-empty array of expressions', ['aliases', 'PathItem'])]], [ [0], - 'Error at #/aliases/PathItem/0: must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset', + [ + new RulesetValidationError( + 'must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset', + ['aliases', 'PathItem'], + ), + ], ], - ])('given %s value used as an alias, throws', (value, error) => { + ])('given %s value used as an alias, throws', (value, errors) => { expect( assertValidRuleset.bind(null, { rules: {}, @@ -424,7 +458,7 @@ Error at #/rules/rule/formats/1: must be a valid format`, PathItem: value, }, }), - ).toThrow(new RulesetValidationError(error)); + ).toThrowAggregateError(new AggregateError(errors)); }); describe('given scoped aliases', () => { @@ -436,10 +470,10 @@ Error at #/rules/rule/formats/1: must be a valid format`, alias: {}, }, }), - ).toThrow( - new RulesetValidationError( - 'Error at #/aliases/alias: targets must be present and have at least a single alias definition', - ), + ).toThrowAggregateError( + new AggregateError([ + new RulesetValidationError('targets must be present and have at least a single alias definition', []), + ]), ); }); @@ -492,7 +526,9 @@ Error at #/rules/rule/formats/1: must be a valid format`, }, }, }), - ).toThrow(new RulesetValidationError('Error at #/aliases/SchemaObject/targets: must be array')); + ).toThrowAggregateError( + new AggregateError([new RulesetValidationError('must be array', ['aliases', 'SchemaObject', 'targets'])]), + ); }); it('demands some target', () => { @@ -505,10 +541,14 @@ Error at #/rules/rule/formats/1: must be a valid format`, }, }, }), - ).toThrow( - new RulesetValidationError( - 'Error at #/aliases/SchemaObject/targets: targets must have at least a single alias definition', - ), + ).toThrowAggregateError( + new AggregateError([ + new RulesetValidationError('targets must have at least a single alias definition', [ + 'aliases', + 'SchemaObject', + 'targets', + ]), + ]), ); }); @@ -522,10 +562,15 @@ Error at #/rules/rule/formats/1: must be a valid format`, }, }, }), - ).toThrow( - new RulesetValidationError( - 'Error at #/aliases/SchemaObject/targets/0: a valid target must contain given and non-empty formats', - ), + ).toThrowAggregateError( + new AggregateError([ + new RulesetValidationError('a valid target must contain given and non-empty formats', [ + 'aliases', + 'SchemaObject', + 'targets', + '0', + ]), + ]), ); }); @@ -548,11 +593,25 @@ Error at #/rules/rule/formats/1: must be a valid format`, }, }, }), - ).toThrow( - new RulesetValidationError( - `Error at #/aliases/SchemaObject/targets/0/formats/0: must be a valid format -Error at #/aliases/SchemaObject/targets/1/formats/1: must be a valid format`, - ), + ).toThrowAggregateError( + new AggregateError([ + new RulesetValidationError('must be a valid format', [ + 'aliases', + 'SchemaObject', + 'targets', + '0', + 'formats', + '0', + ]), + new RulesetValidationError('must be a valid format', [ + 'aliases', + 'SchemaObject', + 'targets', + '1', + 'formats', + '1', + ]), + ]), ); }); @@ -575,10 +634,13 @@ Error at #/aliases/SchemaObject/targets/1/formats/1: must be a valid format`, }, }, }), - ).toThrow( - new RulesetValidationError( - `Error at #/aliases/SchemaObject/targets/1/given: must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset`, - ), + ).toThrowAggregateError( + new AggregateError([ + new RulesetValidationError( + 'must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset', + ['aliases', 'SchemaObject', 'targets', '1', 'given'], + ), + ]), ); }); }); @@ -593,7 +655,7 @@ Error at #/aliases/SchemaObject/targets/1/formats/1: must be a valid format`, rule: { given: '$', then: { - function: 'foo', + function: truthy, }, }, }, @@ -607,7 +669,7 @@ Error at #/aliases/SchemaObject/targets/1/formats/1: must be a valid format`, given: '$', then: { field: 'test', - function: 'foo', + function: truthy, }, }, }, @@ -648,9 +710,17 @@ Error at #/aliases/SchemaObject/targets/1/formats/1: must be a valid format`, duplicateKeys: 'foo', }, }), - ).toThrow( - new RulesetValidationError(`Error at #/parserOptions/duplicateKeys: the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off" -Error at #/parserOptions/incompatibleValues: the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"`), + ).toThrowAggregateError( + new AggregateError([ + new RulesetValidationError( + 'the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"', + ['parserOptions', 'duplicateKeys'], + ), + new RulesetValidationError( + 'the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"', + ['parserOptions', 'incompatibleValues'], + ), + ]), ); }); }); diff --git a/packages/core/src/ruleset/validation/ajv.ts b/packages/core/src/ruleset/validation/ajv.ts new file mode 100644 index 000000000..33f8e34f4 --- /dev/null +++ b/packages/core/src/ruleset/validation/ajv.ts @@ -0,0 +1,69 @@ +import Ajv, { _, ValidateFunction } from 'ajv'; +import addFormats from 'ajv-formats'; +import addErrors from 'ajv-errors'; +import * as ruleSchema from '../meta/rule.schema.json'; +import * as shared from '../meta/shared.json'; +import * as rulesetSchema from '../meta/ruleset.schema.json'; +import * as jsExtensions from '../meta/js-extensions.json'; +import * as jsonExtensions from '../meta/json-extensions.json'; + +const message = _`'spectral-message'`; + +const validators: { [key in 'js' | 'json']: null | ValidateFunction } = { + js: null, + json: null, +}; + +export function createValidator(format: 'js' | 'json'): ValidateFunction { + const existingValidator = validators[format]; + if (existingValidator !== null) { + return existingValidator; + } + + const ajv = new Ajv({ + allErrors: true, + strict: true, + strictRequired: false, + keywords: ['$anchor'], + schemas: [ruleSchema, shared], + }); + addFormats(ajv); + addErrors(ajv); + ajv.addKeyword({ + keyword: 'x-spectral-runtime', + schemaType: 'string', + error: { + message() { + return 'Must match options'; + }, + params(cxt) { + return _`${cxt.data}[Symbol.for(${message})] ? { "errors": ${cxt.data}[Symbol.for(${message})].errors || [${cxt.data}[Symbol.for(${message})]] } : {}`; + }, + }, + code(cxt) { + const { data } = cxt; + + switch (cxt.schema as unknown) { + case 'format': + cxt.fail(_`typeof ${data} !== "function"`); + break; + case 'ruleset-function': + cxt.pass(_`typeof ${data}.function === "function"`); + cxt.pass( + _`(() => { try { ${data}.function.validator && ${data}.function.validator('functionOptions' in ${data} ? ${data}.functionOptions : null); return true; } catch (e) { ${data}[Symbol.for(${message})] = e; return false; } })()`, + ); + break; + } + }, + }); + + if (format === 'js') { + ajv.addSchema(jsExtensions); + } else { + ajv.addSchema(jsonExtensions); + } + + const validator = ajv.compile(rulesetSchema); + validators[format] = validator; + return validator; +} diff --git a/packages/core/src/ruleset/validation/errors.ts b/packages/core/src/ruleset/validation/errors.ts new file mode 100644 index 000000000..0f00eb0e3 --- /dev/null +++ b/packages/core/src/ruleset/validation/errors.ts @@ -0,0 +1,60 @@ +import type { ErrorObject } from 'ajv'; +import type { IDiagnostic, JsonPath } from '@stoplight/types'; + +type RulesetValidationSingleError = Pick; + +export class RulesetValidationError extends Error implements RulesetValidationSingleError { + constructor(public readonly message: string, public readonly path: JsonPath) { + super(message); + } +} + +const RULE_INSTANCE_PATH = /^\/rules\/[^/]+/; +const GENERIC_INSTANCE_PATH = /^\/(?:aliases|extends|overrides(?:\/\d+\/extends)?)/; + +export function convertAjvErrors(errors: ErrorObject[]): RulesetValidationError[] { + const sortedErrors = [...errors] + .sort((errorA, errorB) => { + const diff = errorA.instancePath.length - errorB.instancePath.length; + return diff === 0 ? (errorA.keyword === 'errorMessage' && errorB.keyword !== 'errorMessage' ? -1 : 0) : diff; + }) + .filter((error, i, sortedErrors) => i === 0 || sortedErrors[i - 1].instancePath !== error.instancePath); + + const filteredErrors: ErrorObject[] = []; + + l: for (let i = 0; i < sortedErrors.length; i++) { + const error = sortedErrors[i]; + const prevError = i === 0 ? null : sortedErrors[i - 1]; + + if (GENERIC_INSTANCE_PATH.test(error.instancePath)) { + let x = 1; + while (i + x < sortedErrors.length) { + if ( + sortedErrors[i + x].instancePath.startsWith(error.instancePath) || + !GENERIC_INSTANCE_PATH.test(sortedErrors[i + x].instancePath) + ) { + continue l; + } + + x++; + } + } else if (prevError === null) { + filteredErrors.push(error); + continue; + } else { + const match = RULE_INSTANCE_PATH.exec(error.instancePath); + + if (match !== null && match[0] !== match.input && match[0] === prevError.instancePath) { + filteredErrors.pop(); + } + } + + filteredErrors.push(error); + } + + return filteredErrors.flatMap(error => + error.keyword === 'x-spectral-runtime' + ? (error.params.errors as RulesetValidationError[]) + : new RulesetValidationError(error.message ?? 'unknown error', error.instancePath.slice(1).split('/')), + ); +} diff --git a/packages/core/src/ruleset/validation/index.ts b/packages/core/src/ruleset/validation/index.ts new file mode 100644 index 000000000..80ce166e1 --- /dev/null +++ b/packages/core/src/ruleset/validation/index.ts @@ -0,0 +1,39 @@ +import { isPlainObject } from '@stoplight/json'; +import AggregateError = require('es-aggregate-error'); + +import type { FileRuleDefinition, RuleDefinition, RulesetDefinition } from '../types'; +import { createValidator } from './ajv'; +import { convertAjvErrors } from './errors'; + +import { RulesetValidationError } from './errors'; + +export { RulesetValidationError }; + +export function assertValidRuleset( + ruleset: unknown, + format: 'js' | 'json' = 'js', +): asserts ruleset is RulesetDefinition { + if (!isPlainObject(ruleset)) { + throw new RulesetValidationError('Provided ruleset is not an object', []); + } + + if (!('rules' in ruleset) && !('extends' in ruleset) && !('overrides' in ruleset)) { + throw new RulesetValidationError('Ruleset must have rules or extends or overrides defined', []); + } + + const validate = createValidator(format); + + if (!validate(ruleset)) { + throw new AggregateError(convertAjvErrors(validate.errors ?? [])); + } +} + +function isRuleDefinition(rule: FileRuleDefinition): rule is RuleDefinition { + return typeof rule === 'object' && rule !== null && !Array.isArray(rule) && ('given' in rule || 'then' in rule); +} + +export function assertValidRule(rule: FileRuleDefinition, name: string): asserts rule is RuleDefinition { + if (!isRuleDefinition(rule)) { + throw new RulesetValidationError('Rule definition expected', ['rules', name]); + } +} diff --git a/packages/functions/src/__tests__/__helpers__/tester.ts b/packages/functions/src/__tests__/__helpers__/tester.ts index 0dc0c0a9b..8bf0d6a39 100644 --- a/packages/functions/src/__tests__/__helpers__/tester.ts +++ b/packages/functions/src/__tests__/__helpers__/tester.ts @@ -6,7 +6,9 @@ import { IRuleResult, RulesetFunction, RulesetFunctionWithValidator, + RulesetValidationError, } from '@stoplight/spectral-core'; +import { isAggregateError } from '@stoplight/spectral-core/src/guards/isAggregateError'; export default async function ( // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -16,40 +18,37 @@ export default async function ( rule?: Partial> & { then?: Partial }, ): Promise[]> { const s = new Spectral(); - s.setRuleset({ - rules: { - 'my-rule': { - given: '$', - ...rule, - then: { - ...(rule?.then as Ruleset['rules']['then']), - function: fn, - functionOptions: opts, + try { + s.setRuleset({ + rules: { + 'my-rule': { + given: '$', + ...rule, + then: { + ...(rule?.then as Ruleset['rules']['then']), + function: fn, + functionOptions: opts, + }, }, }, - }, - }); - - try { - const results = await s.run(input instanceof Document ? input : JSON.stringify(input)); - return results - .filter(result => result.code === 'my-rule') - .map(error => ({ - path: error.path, - message: error.message, - })); - } catch (e: unknown) { - if ( - e instanceof Error && - Array.isArray((e as Error & { errors?: unknown }).errors) && - (e as Error & { errors: unknown[] }).errors.length === 1 - ) { - const actualError = (e as Error & { errors: [unknown] }).errors[0]; - throw actualError instanceof Error && 'cause' in (actualError as Error & { cause?: unknown }) - ? (actualError as Error & { cause: unknown }).cause - : actualError; - } else { - throw e; + }); + } catch (ex) { + if (isAggregateError(ex)) { + for (const e of ex.errors) { + if (e instanceof RulesetValidationError) { + e.path.length = 0; + } + } } + + throw ex; } + + const results = await s.run(input instanceof Document ? input : JSON.stringify(input)); + return results + .filter(result => result.code === 'my-rule') + .map(error => ({ + path: error.path, + message: error.message, + })); } diff --git a/packages/functions/src/__tests__/alphabetical.test.ts b/packages/functions/src/__tests__/alphabetical.test.ts index 7ef63b41a..f2883ba73 100644 --- a/packages/functions/src/__tests__/alphabetical.test.ts +++ b/packages/functions/src/__tests__/alphabetical.test.ts @@ -2,9 +2,12 @@ import * as Parsers from '@stoplight/spectral-parsers'; import { Document, RulesetValidationError } from '@stoplight/spectral-core'; import testFunction from './__helpers__/tester'; import alphabetical from '../alphabetical'; +import AggregateError = require('es-aggregate-error'); const runAlphabetical = testFunction.bind(null, alphabetical); +import '@stoplight/spectral-test-utils/matchers'; + describe('Core Functions / Alphabetical', () => { it('given falsy target, should return no error message', async () => { expect(await runAlphabetical(false)).toEqual([]); @@ -137,7 +140,9 @@ describe('Core Functions / Alphabetical', () => { ], [{ keyedBy: 2 }, '"alphabetical" function and its "keyedBy" option accepts only the following types: string'], ])('given invalid %p options, should throw', async (opts, error) => { - await expect(runAlphabetical([], opts)).rejects.toThrow(new RulesetValidationError(error)); + await expect(runAlphabetical([], opts)).rejects.toThrowAggregateError( + new AggregateError([new RulesetValidationError(error, [])]), + ); }); }); }); diff --git a/packages/functions/src/__tests__/casing.test.ts b/packages/functions/src/__tests__/casing.test.ts index 612a220f5..24150bcf9 100644 --- a/packages/functions/src/__tests__/casing.test.ts +++ b/packages/functions/src/__tests__/casing.test.ts @@ -1,6 +1,8 @@ +import '@stoplight/spectral-test-utils/matchers'; import { RulesetValidationError } from '@stoplight/spectral-core'; import casing, { CasingType } from '../casing'; import testFunction from './__helpers__/tester'; +import AggregateError = require('es-aggregate-error'); const runCasing = testFunction.bind(null, casing); @@ -372,19 +374,27 @@ describe('Core Functions / Casing', () => { await expect(runCasing('foo', opts)).resolves.toBeInstanceOf(Array); }); - it.each<[unknown, string]>([ + it.each<[unknown, RulesetValidationError[]]>([ [ { type: 'foo' }, - '"casing" function and its "type" option accept the following values: flat, camel, pascal, kebab, cobol, snake, macro', + [ + new RulesetValidationError( + '"casing" function and its "type" option accept the following values: flat, camel, pascal, kebab, cobol, snake, macro', + [], + ), + ], + ], + [ + { type: 'macro', foo: true }, + [new RulesetValidationError('"casing" function does not support "foo" option', [])], ], - [{ type: 'macro', foo: true }, '"casing" function does not support "foo" option'], [ { type: 'pascal', disallowDigits: false, separator: {}, }, - '"casing" function is missing "separator.char" option', + [new RulesetValidationError('"casing" function is missing "separator.char" option', [])], ], [ { @@ -392,11 +402,11 @@ describe('Core Functions / Casing', () => { disallowDigits: false, separator: { allowLeading: true }, }, - '"casing" function is missing "separator.char" option', + [new RulesetValidationError('"casing" function is missing "separator.char" option', [])], ], [ { type: 'snake', separator: { char: 'a', foo: true } }, - '"casing" function does not support "separator.foo" option', + [new RulesetValidationError('"casing" function does not support "separator.foo" option', [])], ], [ { @@ -405,7 +415,12 @@ describe('Core Functions / Casing', () => { char: 'fo', }, }, - '"casing" function and its "separator.char" option accepts only char, i.e. "I" or "/"', + [ + new RulesetValidationError( + '"casing" function and its "separator.char" option accepts only char, i.e. "I" or "/"', + [], + ), + ], ], [ { @@ -414,10 +429,15 @@ describe('Core Functions / Casing', () => { char: null, }, }, - '"casing" function and its "separator.char" option accepts only char, i.e. "I" or "/"', + [ + new RulesetValidationError( + '"casing" function and its "separator.char" option accepts only char, i.e. "I" or "/"', + [], + ), + ], ], - ])('given invalid %p options, should throw', async (opts, error) => { - await expect(runCasing('foo', opts)).rejects.toThrow(new RulesetValidationError(error)); + ])('given invalid %p options, should throw', async (opts, errors) => { + await expect(runCasing('foo', opts)).rejects.toThrowAggregateError(new AggregateError(errors)); }); }); }); diff --git a/packages/functions/src/__tests__/defined.test.ts b/packages/functions/src/__tests__/defined.test.ts index 18a848870..deede0bfd 100644 --- a/packages/functions/src/__tests__/defined.test.ts +++ b/packages/functions/src/__tests__/defined.test.ts @@ -1,6 +1,9 @@ +import '@stoplight/spectral-test-utils/matchers'; + import defined from '../defined'; import testFunction from './__helpers__/tester'; import { RulesetValidationError } from '@stoplight/spectral-core'; +import AggregateError = require('es-aggregate-error'); const runDefined = testFunction.bind(null, defined); @@ -20,8 +23,8 @@ describe('Core Functions / Defined', () => { describe('validation', () => { it.each([{}, 2])('given invalid %p options, should throw', async opts => { - await expect(runDefined([], opts)).rejects.toThrow( - new RulesetValidationError('"defined" function does not accept any options'), + await expect(runDefined([], opts)).rejects.toThrowAggregateError( + new AggregateError([new RulesetValidationError('"defined" function does not accept any options', [])]), ); }); }); diff --git a/packages/functions/src/__tests__/enumeration.test.ts b/packages/functions/src/__tests__/enumeration.test.ts index eba56c0e5..23dfe379f 100644 --- a/packages/functions/src/__tests__/enumeration.test.ts +++ b/packages/functions/src/__tests__/enumeration.test.ts @@ -1,6 +1,9 @@ +import '@stoplight/spectral-test-utils/matchers'; + import { RulesetValidationError } from '@stoplight/spectral-core'; import enumeration from '../enumeration'; import testFunction from './__helpers__/tester'; +import AggregateError = require('es-aggregate-error'); const runEnumeration = testFunction.bind(null, enumeration); @@ -31,30 +34,45 @@ describe('Core Functions / Enumeration', () => { ).toEqual([]); }); - it.each<[unknown, string]>([ + it.each<[unknown, RulesetValidationError[]]>([ [ { values: ['foo', 2], foo: true, }, - '"enumeration" function does not support "foo" option', + [new RulesetValidationError('"enumeration" function does not support "foo" option', [])], ], [ { values: [{}], }, - '"enumeration" and its "values" option support only arrays of primitive values, i.e. ["Berlin", "London", "Paris"]', + [ + new RulesetValidationError( + '"enumeration" and its "values" option support only arrays of primitive values, i.e. ["Berlin", "London", "Paris"]', + [], + ), + ], ], [ null, - '"enumeration" function has invalid options specified. Example valid options: { "values": ["Berlin", "London", "Paris"] }, { "values": [2, 3, 5, 8, 13, 21] }', + [ + new RulesetValidationError( + '"enumeration" function has invalid options specified. Example valid options: { "values": ["Berlin", "London", "Paris"] }, { "values": [2, 3, 5, 8, 13, 21] }', + [], + ), + ], ], [ 2, - '"enumeration" function has invalid options specified. Example valid options: { "values": ["Berlin", "London", "Paris"] }, { "values": [2, 3, 5, 8, 13, 21] }', + [ + new RulesetValidationError( + '"enumeration" function has invalid options specified. Example valid options: { "values": ["Berlin", "London", "Paris"] }, { "values": [2, 3, 5, 8, 13, 21] }', + [], + ), + ], ], - ])('given invalid %p options, should throw', async (opts, error) => { - await expect(runEnumeration('foo', opts)).rejects.toThrow(new RulesetValidationError(error)); + ])('given invalid %p options, should throw', async (opts, errors) => { + await expect(runEnumeration('foo', opts)).rejects.toThrowAggregateError(new AggregateError(errors)); }); }); }); diff --git a/packages/functions/src/__tests__/falsy.test.ts b/packages/functions/src/__tests__/falsy.test.ts index 1f5773318..13f620395 100644 --- a/packages/functions/src/__tests__/falsy.test.ts +++ b/packages/functions/src/__tests__/falsy.test.ts @@ -1,6 +1,9 @@ +import '@stoplight/spectral-test-utils/matchers'; + import { RulesetValidationError } from '@stoplight/spectral-core'; import falsy from '../falsy'; import testFunction from './__helpers__/tester'; +import AggregateError = require('es-aggregate-error'); const runFalsy = testFunction.bind(null, falsy); @@ -20,8 +23,8 @@ describe('Core Functions / Falsy', () => { describe('validation', () => { it.each([{}, 2])('given invalid %p options, should throw', async opts => { - await expect(runFalsy([], opts)).rejects.toThrow( - new RulesetValidationError('"falsy" function does not accept any options'), + await expect(runFalsy([], opts)).rejects.toThrowAggregateError( + new AggregateError([new RulesetValidationError('"falsy" function does not accept any options', [])]), ); }); }); diff --git a/packages/functions/src/__tests__/length.test.ts b/packages/functions/src/__tests__/length.test.ts index 816c909a7..54ffd46f2 100644 --- a/packages/functions/src/__tests__/length.test.ts +++ b/packages/functions/src/__tests__/length.test.ts @@ -1,6 +1,8 @@ import { RulesetValidationError } from '@stoplight/spectral-core'; import testFunction from './__helpers__/tester'; import length from '../length'; +import '@stoplight/spectral-test-utils/matchers'; +import AggregateError = require('es-aggregate-error'); const runLength = testFunction.bind(null, length); @@ -52,31 +54,66 @@ describe('Core Functions / Length', () => { expect(await runLength('foo', opts)).toEqual([]); }); - it.each<[unknown, string]>([ + it.each<[unknown, RulesetValidationError[]]>([ [ null, - '"length" function has invalid options specified. Example valid options: { "min": 2 }, { "max": 5 }, { "min": 0, "max": 10 }', + [ + new RulesetValidationError( + '"length" function has invalid options specified. Example valid options: { "min": 2 }, { "max": 5 }, { "min": 0, "max": 10 }', + [], + ), + ], ], [ 2, - '"length" function has invalid options specified. Example valid options: { "min": 2 }, { "max": 5 }, { "min": 0, "max": 10 }', + [ + new RulesetValidationError( + '"length" function has invalid options specified. Example valid options: { "min": 2 }, { "max": 5 }, { "min": 0, "max": 10 }', + [], + ), + ], ], [ { min: 2, foo: true, }, - '"length" function does not support "foo" option', + [new RulesetValidationError('"length" function does not support "foo" option', [])], + ], + [ + { min: '2' }, + [ + new RulesetValidationError( + '"length" function and its "min" option accepts only the following types: number', + [], + ), + ], + ], + + [ + { max: '2' }, + [ + new RulesetValidationError( + `"length" function and its "max" option accepts only the following types: number`, + [], + ), + ], ], - [{ min: '2' }, '"length" function and its "min" option accepts only the following types: number'], - [{ max: '2' }, `"length" function and its "max" option accepts only the following types: number`], [ { min: '4', max: '2' }, - `"length" function and its "min" option accepts only the following types: number -"length" function and its "max" option accepts only the following types: number`, + [ + new RulesetValidationError( + `"length" function and its "min" option accepts only the following types: number`, + [], + ), + new RulesetValidationError( + `"length" function and its "max" option accepts only the following types: number`, + [], + ), + ], ], - ])('given invalid %p options, should throw', async (opts, error) => { - await expect(runLength('foo', opts)).rejects.toThrow(new RulesetValidationError(error)); + ])('given invalid %p options, should throw', async (opts, errors) => { + await expect(runLength('foo', opts)).rejects.toThrowAggregateError(new AggregateError(errors)); }); }); }); diff --git a/packages/functions/src/__tests__/pattern.test.ts b/packages/functions/src/__tests__/pattern.test.ts index 200912099..cedaf91e6 100644 --- a/packages/functions/src/__tests__/pattern.test.ts +++ b/packages/functions/src/__tests__/pattern.test.ts @@ -1,6 +1,9 @@ +import '@stoplight/spectral-test-utils/matchers'; + import { RulesetValidationError } from '@stoplight/spectral-core'; import testFunction from './__helpers__/tester'; import pattern from '../pattern'; +import AggregateError = require('es-aggregate-error'); const runPattern = testFunction.bind(null, pattern); @@ -43,25 +46,51 @@ describe('Core Functions / Pattern', () => { }, ); - it.each<[unknown, string]>([ + it.each<[unknown, RulesetValidationError[]]>([ [ null, - '"pattern" function has invalid options specified. Example valid options: { "match": "^Stoplight" }, { "notMatch": "Swagger" }, { "match": "Stoplight", "notMatch": "Swagger" }', + [ + new RulesetValidationError( + '"pattern" function has invalid options specified. Example valid options: { "match": "^Stoplight" }, { "notMatch": "Swagger" }, { "match": "Stoplight", "notMatch": "Swagger" }', + [], + ), + ], ], [ {}, - `"pattern" function has invalid options specified. Example valid options: { "match": "^Stoplight" }, { "notMatch": "Swagger" }, { "match": "Stoplight", "notMatch": "Swagger" }`, + [ + new RulesetValidationError( + `"pattern" function has invalid options specified. Example valid options: { "match": "^Stoplight" }, { "notMatch": "Swagger" }, { "match": "Stoplight", "notMatch": "Swagger" }`, + [], + ), + ], + ], + [{ foo: true }, [new RulesetValidationError('"pattern" function does not support "foo" option', [])]], + [ + { match: 2 }, + [new RulesetValidationError('"pattern" function and its "match" option must be string or RegExp instance', [])], + ], + [ + { notMatch: null }, + [ + new RulesetValidationError( + '"pattern" function and its "notMatch" option must be string or RegExp instance', + [], + ), + ], ], - [{ foo: true }, '"pattern" function does not support "foo" option'], - [{ match: 2 }, '"pattern" function and its "match" option must be string or RegExp instance'], - [{ notMatch: null }, '"pattern" function and its "notMatch" option must be string or RegExp instance'], [ { match: 4, notMatch: 10 }, - `"pattern" function and its "match" option must be string or RegExp instance -"pattern" function and its "notMatch" option must be string or RegExp instance`, + [ + new RulesetValidationError(`"pattern" function and its "match" option must be string or RegExp instance`, []), + new RulesetValidationError( + `"pattern" function and its "notMatch" option must be string or RegExp instance`, + [], + ), + ], ], - ])('given invalid %p options, should throw', async (opts, error) => { - await expect(runPattern('abc', opts)).rejects.toThrow(new RulesetValidationError(error)); + ])('given invalid %p options, should throw', async (opts, errors) => { + await expect(runPattern('abc', opts)).rejects.toThrowAggregateError(new AggregateError(errors)); }); }); }); diff --git a/packages/functions/src/__tests__/schema.test.ts b/packages/functions/src/__tests__/schema.test.ts index a5cd6cb49..562226d5f 100644 --- a/packages/functions/src/__tests__/schema.test.ts +++ b/packages/functions/src/__tests__/schema.test.ts @@ -2,6 +2,8 @@ import type { JSONSchema6 as JSONSchema } from 'json-schema'; import schema from '../schema'; import { RulesetValidationError } from '@stoplight/spectral-core'; import testFunction from './__helpers__/tester'; +import '@stoplight/spectral-test-utils/matchers'; +import AggregateError = require('es-aggregate-error'); const runSchema = testFunction.bind(null, schema); @@ -476,28 +478,57 @@ describe('Core Functions / Schema', () => { expect(await runSchema('', opts)).toBeInstanceOf(Array); }); - it.each<[unknown, string]>([ + it.each<[unknown, RulesetValidationError[]]>([ [ 2, - '"schema" function has invalid options specified. Example valid options: { "schema": { /* any JSON Schema can be defined here */ } , { "schema": { "type": "object" }, "dialect": "auto" }', + [ + new RulesetValidationError( + '"schema" function has invalid options specified. Example valid options: { "schema": { /* any JSON Schema can be defined here */ } , { "schema": { "type": "object" }, "dialect": "auto" }', + [], + ), + ], + ], + [ + { schema: { type: 'object' }, foo: true }, + [new RulesetValidationError('"schema" function does not support "foo" option', [])], + ], + [ + { schema: { type: 'object' }, oasVersion: 1 }, + [new RulesetValidationError('"schema" function does not support "oasVersion" option', [])], ], - [{ schema: { type: 'object' }, foo: true }, '"schema" function does not support "foo" option'], - [{ schema: { type: 'object' }, oasVersion: 1 }, '"schema" function does not support "oasVersion" option'], [ { schema: { type: 'object' }, dialect: 'foo' }, - '"schema" function and its "dialect" option accepts only the following values: "auto", "draft4", "draft6", "draft7", "draft2019-09", "draft2020-12"', + [ + new RulesetValidationError( + '"schema" function and its "dialect" option accepts only the following values: "auto", "draft4", "draft6", "draft7", "draft2019-09", "draft2020-12"', + [], + ), + ], ], [ { schema: { type: 'object' }, allErrors: null }, - '"schema" function and its "allErrors" option accepts only the following types: boolean', + [ + new RulesetValidationError( + '"schema" function and its "allErrors" option accepts only the following types: boolean', + [], + ), + ], ], [ { schema: null, allErrors: null }, - `"schema" function and its "schema" option accepts only the following types: object -"schema" function and its "allErrors" option accepts only the following types: boolean`, + [ + new RulesetValidationError( + `"schema" function and its "schema" option accepts only the following types: object`, + [], + ), + new RulesetValidationError( + `"schema" function and its "allErrors" option accepts only the following types: boolean`, + [], + ), + ], ], - ])('given invalid %p options, should throw', async (opts, error) => { - await expect(runSchema([], opts)).rejects.toThrow(new RulesetValidationError(error)); + ])('given invalid %p options, should throw', async (opts, errors) => { + await expect(runSchema([], opts)).rejects.toThrowAggregateError(new AggregateError(errors)); }); }); }); diff --git a/packages/functions/src/__tests__/truthy.test.ts b/packages/functions/src/__tests__/truthy.test.ts index 2a8f088c7..d49ff8ab7 100644 --- a/packages/functions/src/__tests__/truthy.test.ts +++ b/packages/functions/src/__tests__/truthy.test.ts @@ -1,6 +1,9 @@ +import '@stoplight/spectral-test-utils/matchers'; + import { RulesetValidationError } from '@stoplight/spectral-core'; import truthy from '../truthy'; import testFunction from './__helpers__/tester'; +import AggregateError = require('es-aggregate-error'); const runTruthy = testFunction.bind(null, truthy); @@ -20,8 +23,8 @@ describe('Core Functions / Truthy', () => { describe('validation', () => { it.each([{}, 2])('given invalid %p options, should throw', async opts => { - await expect(runTruthy([], opts)).rejects.toThrow( - new RulesetValidationError('"truthy" function does not accept any options'), + await expect(runTruthy([], opts)).rejects.toThrowAggregateError( + new AggregateError([new RulesetValidationError('"truthy" function does not accept any options', [])]), ); }); }); diff --git a/packages/functions/src/__tests__/unreferencedReusableObject.test.ts b/packages/functions/src/__tests__/unreferencedReusableObject.test.ts index 98324c130..c49bee68c 100644 --- a/packages/functions/src/__tests__/unreferencedReusableObject.test.ts +++ b/packages/functions/src/__tests__/unreferencedReusableObject.test.ts @@ -1,6 +1,9 @@ +import '@stoplight/spectral-test-utils/matchers'; + import { RulesetValidationError } from '@stoplight/spectral-core'; import testFunction from './__helpers__/tester'; import unreferencedReusableObject from '../unreferencedReusableObject'; +import AggregateError = require('es-aggregate-error'); const runUnreferencedReusableObject = testFunction.bind(null, unreferencedReusableObject); @@ -24,40 +27,65 @@ describe('Core Functions / UnreferencedReusableObject', () => { expect(await runUnreferencedReusableObject({}, opts)).toEqual([]); }); - it.each<[unknown, string]>([ + it.each<[unknown, RulesetValidationError[]]>([ [ null, - '"unreferencedReusableObject" function has invalid options specified. Example valid options: { "reusableObjectsLocation": "#/components/schemas" }, { "reusableObjectsLocation": "#/$defs" }', + [ + new RulesetValidationError( + '"unreferencedReusableObject" function has invalid options specified. Example valid options: { "reusableObjectsLocation": "#/components/schemas" }, { "reusableObjectsLocation": "#/$defs" }', + [], + ), + ], ], [ 2, - '"unreferencedReusableObject" function has invalid options specified. Example valid options: { "reusableObjectsLocation": "#/components/schemas" }, { "reusableObjectsLocation": "#/$defs" }', + [ + new RulesetValidationError( + '"unreferencedReusableObject" function has invalid options specified. Example valid options: { "reusableObjectsLocation": "#/components/schemas" }, { "reusableObjectsLocation": "#/$defs" }', + [], + ), + ], ], [ {}, - '"unreferencedReusableObject" function is missing "reusableObjectsLocation" option. Example valid options: { "reusableObjectsLocation": "#/components/schemas" }, { "reusableObjectsLocation": "#/$defs" }', + [ + new RulesetValidationError( + '"unreferencedReusableObject" function is missing "reusableObjectsLocation" option. Example valid options: { "reusableObjectsLocation": "#/components/schemas" }, { "reusableObjectsLocation": "#/$defs" }', + [], + ), + ], ], [ { reusableObjectsLocation: '#', foo: true, }, - '"unreferencedReusableObject" function does not support "foo" option', + [new RulesetValidationError('"unreferencedReusableObject" function does not support "foo" option', [])], ], [ { reusableObjectsLocation: 2, }, - '"unreferencedReusableObject" and its "reusableObjectsLocation" option support only valid JSON Pointer fragments, i.e. "#", "#/foo", "#/paths/~1user"', + [ + new RulesetValidationError( + '"unreferencedReusableObject" and its "reusableObjectsLocation" option support only valid JSON Pointer fragments, i.e. "#", "#/foo", "#/paths/~1user"', + [], + ), + ], ], [ { reusableObjectsLocation: 'foo', }, - '"unreferencedReusableObject" and its "reusableObjectsLocation" option support only valid JSON Pointer fragments, i.e. "#", "#/foo", "#/paths/~1user"', + [ + new RulesetValidationError( + '"unreferencedReusableObject" and its "reusableObjectsLocation" option support only valid JSON Pointer fragments, i.e. "#", "#/foo", "#/paths/~1user"', + [], + ), + ], ], - ])('given invalid %p options, should throw', async (opts, error) => { - await expect(runUnreferencedReusableObject({}, opts)).rejects.toThrow(new RulesetValidationError(error)); + ])('given invalid %p options, should throw', async (opts, errors) => { + await expect(runUnreferencedReusableObject({}, opts)).rejects.toThrowAggregateError(new AggregateError(errors)); }); }); }); diff --git a/packages/functions/src/__tests__/xor.test.ts b/packages/functions/src/__tests__/xor.test.ts index b93c96137..908f8e3d1 100644 --- a/packages/functions/src/__tests__/xor.test.ts +++ b/packages/functions/src/__tests__/xor.test.ts @@ -1,6 +1,9 @@ +import '@stoplight/spectral-test-utils/matchers'; + import { RulesetValidationError } from '@stoplight/spectral-core'; import testFunction from './__helpers__/tester'; import xor from '../xor'; +import AggregateError = require('es-aggregate-error'); const runXor = testFunction.bind(null, xor); @@ -63,25 +66,67 @@ describe('Core Functions / Xor', () => { expect(await runXor([], opts)).toEqual([]); }); - it.each<[unknown, string]>([ + it.each<[unknown, RulesetValidationError[]]>([ [ null, - '"xor" function has invalid options specified. Example valid options: { "properties": ["id", "name"] }, { "properties": ["country", "street"] }', + [ + new RulesetValidationError( + '"xor" function has invalid options specified. Example valid options: { "properties": ["id", "name"] }, { "properties": ["country", "street"] }', + [], + ), + ], ], [ 2, - '"xor" function has invalid options specified. Example valid options: { "properties": ["id", "name"] }, { "properties": ["country", "street"] }', + [ + new RulesetValidationError( + '"xor" function has invalid options specified. Example valid options: { "properties": ["id", "name"] }, { "properties": ["country", "street"] }', + [], + ), + ], + ], + [ + { properties: ['foo', 'bar'], foo: true }, + [new RulesetValidationError('"xor" function does not support "foo" option', [])], ], - [{ properties: ['foo', 'bar'], foo: true }, '"xor" function does not support "foo" option'], [ { properties: ['foo', 'bar', 'baz'] }, - '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]', + [ + new RulesetValidationError( + '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]', + [], + ), + ], + ], + [ + { properties: ['foo', {}] }, + [ + new RulesetValidationError( + '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]', + [], + ), + ], + ], + [ + { properties: ['foo'] }, + [ + new RulesetValidationError( + '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]', + [], + ), + ], + ], + [ + { properties: [] }, + [ + new RulesetValidationError( + '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]', + [], + ), + ], ], - [{ properties: ['foo', {}] }, '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]'], - [{ properties: ['foo'] }, '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]'], - [{ properties: [] }, '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]'], - ])('given invalid %p options, should throw', async (opts, error) => { - await expect(runXor({}, opts)).rejects.toThrow(new RulesetValidationError(error)); + ])('given invalid %p options, should throw', async (opts, errors) => { + await expect(runXor({}, opts)).rejects.toThrowAggregateError(new AggregateError(errors)); }); }); }); diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 80ecc9ad2..97680c0a2 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@stoplight/spectral-runtime", - "version": "1.1.1", + "version": "1.1.0", "homepage": "https://github.com/stoplightio/spectral", "bugs": "https://github.com/stoplightio/spectral/issues", "author": "Stoplight ", @@ -21,12 +21,11 @@ "release": "semantic-release -e semantic-release-monorepo" }, "dependencies": { - "@stoplight/json": "^3.17.0", + "@stoplight/json": "^3.15.0", "@stoplight/path": "^1.3.2", "@stoplight/types": "^12.3.0", "abort-controller": "^3.0.0", "lodash": "^4.17.21", - "node-fetch": "^2.6.1", - "tslib": "^2.3.1" + "node-fetch": "^2.6.1" } } diff --git a/test-utils/index.ts b/test-utils/index.ts index 1bd360767..f7a90b84a 100644 --- a/test-utils/index.ts +++ b/test-utils/index.ts @@ -1,6 +1,4 @@ -declare module '@stoplight/spectral-test-utils' { - type Body = string | Record; +type Body = string | Record; - export function serveAssets(mocks: Record): void; - export function mockResponses(mocks: Record>): void; -} +export declare function serveAssets(mocks: Record): void; +export declare function mockResponses(mocks: Record>): void; diff --git a/test-utils/matchers.ts b/test-utils/matchers.ts new file mode 100644 index 000000000..ca1376f09 --- /dev/null +++ b/test-utils/matchers.ts @@ -0,0 +1,34 @@ +import AggregateError = require('es-aggregate-error'); + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + interface Matchers { + toThrowAggregateError(error: AggregateError): R; + } + } +} + +expect.extend({ + toThrowAggregateError(received, expected) { + let error: unknown; + if (typeof received === 'function') { + expect(received).toThrow(AggregateError); + try { + received(); + } catch (e) { + error = e; + } + } else { + error = received; + } + + expect(error).toBeInstanceOf(Error); + expect((error as AggregateError).errors).toEqual(expected.errors); + + return { + message: () => 'All errors matched!', + pass: true, + }; + }, +}); diff --git a/tsconfig.json b/tsconfig.json index f57c5fa8c..0f4dc1c03 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,9 @@ "@stoplight/spectral-ruleset-migrator": ["packages/ruleset-migrator/src/index.ts"], "@stoplight/spectral-rulesets": ["packages/rulesets/src/index.ts"], + "@stoplight/spectral-test-utils": ["test-utils/index.ts"], + "@stoplight/spectral-test-utils/matchers": ["test-utils/matchers.ts"], + "nimma/fallbacks": ["node_modules/nimma/dist/cjs/fallbacks/"], "nimma/legacy": ["node_modules/nimma/dist/legacy/cjs/"] }, diff --git a/yarn.lock b/yarn.lock index 614fa39b8..10f1cbe10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1935,7 +1935,7 @@ __metadata: languageName: node linkType: hard -"@stoplight/json@npm:3.17.0, @stoplight/json@npm:^3.17.0, @stoplight/json@npm:~3.17.0, @stoplight/json@npm:~3.17.1": +"@stoplight/json@npm:3.17.0, @stoplight/json@npm:^3.15.0, @stoplight/json@npm:^3.17.0, @stoplight/json@npm:~3.17.1": version: 3.17.0 resolution: "@stoplight/json@npm:3.17.0" dependencies: @@ -1948,6 +1948,19 @@ __metadata: languageName: node linkType: hard +"@stoplight/json@npm:~3.17.0": + version: 3.17.1 + resolution: "@stoplight/json@npm:3.17.1" + dependencies: + "@stoplight/ordered-object-literal": ^1.0.2 + "@stoplight/types": ^12.3.0 + jsonc-parser: ~2.2.1 + lodash: ^4.17.21 + safe-stable-stringify: ^1.1 + checksum: 4a9ce5812c8ec6415de06608c99c718ecffc5a8db8488fedbc50eae47b0b287dd1f54d06576f7c4b958f3fdc847c7aad671ae918db0c00db868c2cba7781eebd + languageName: node + linkType: hard + "@stoplight/lifecycle@npm:2.3.2": version: 2.3.2 resolution: "@stoplight/lifecycle@npm:2.3.2" @@ -2023,8 +2036,9 @@ __metadata: "@stoplight/spectral-parsers": "*" "@stoplight/spectral-ref-resolver": ^1.0.0 "@stoplight/spectral-runtime": ^1.0.0 - "@stoplight/types": 12.3.0 + "@stoplight/types": 12.5.0 "@stoplight/yaml": ^4.2.2 + "@types/es-aggregate-error": ^1.0.2 "@types/json-schema": ^7.0.7 "@types/minimatch": ^3.0.5 "@types/treeify": ^1.0.0 @@ -2032,7 +2046,7 @@ __metadata: ajv-errors: ~3.0.0 ajv-formats: ~2.1.0 blueimp-md5: 2.18.0 - json-schema: 0.4.0 + es-aggregate-error: ^1.0.7 jsonpath-plus: 6.0.1 lodash: ~4.17.21 lodash.topath: ^4.5.2 @@ -2175,20 +2189,34 @@ __metadata: languageName: unknown linkType: soft -"@stoplight/spectral-runtime@^1.0.0, @stoplight/spectral-runtime@^1.1.0, @stoplight/spectral-runtime@^1.1.1, @stoplight/spectral-runtime@workspace:packages/runtime": +"@stoplight/spectral-runtime@^1.0.0, @stoplight/spectral-runtime@^1.1.0, @stoplight/spectral-runtime@workspace:packages/runtime": version: 0.0.0-use.local resolution: "@stoplight/spectral-runtime@workspace:packages/runtime" dependencies: - "@stoplight/json": ^3.17.0 + "@stoplight/json": ^3.15.0 "@stoplight/path": ^1.3.2 "@stoplight/types": ^12.3.0 abort-controller: ^3.0.0 lodash: ^4.17.21 node-fetch: ^2.6.1 - tslib: ^2.3.1 languageName: unknown linkType: soft +"@stoplight/spectral-runtime@npm:^1.1.1": + version: 1.1.1 + resolution: "@stoplight/spectral-runtime@npm:1.1.1" + dependencies: + "@stoplight/json": ^3.17.0 + "@stoplight/path": ^1.3.2 + "@stoplight/types": ^12.3.0 + abort-controller: ^3.0.0 + lodash: ^4.17.21 + node-fetch: ^2.6.1 + tslib: ^2.3.1 + checksum: 6078e6a610b26a906c0348a8a5f903ca9f61d0fd52f8ea342d48ef35459147d63d612593efe2fcc31b64fe79eaac15fe2a609c5a3d5404c23bbb3c4f7590c4fb + languageName: node + linkType: hard + "@stoplight/types@npm:12.3.0, @stoplight/types@npm:^12.0.0, @stoplight/types@npm:^12.3.0": version: 12.3.0 resolution: "@stoplight/types@npm:12.3.0" @@ -2199,6 +2227,16 @@ __metadata: languageName: node linkType: hard +"@stoplight/types@npm:12.5.0": + version: 12.5.0 + resolution: "@stoplight/types@npm:12.5.0" + dependencies: + "@types/json-schema": ^7.0.4 + utility-types: ^3.10.0 + checksum: fe4a09df6e1c2f0cdb53f474b180cc7b8184e814e1ac4427d199642f10958335f597060530a908c0e5800ba2569d077afe124a51deaee466255ce942e1e03941 + languageName: node + linkType: hard + "@stoplight/yaml-ast-parser@npm:0.0.48": version: 0.0.48 resolution: "@stoplight/yaml-ast-parser@npm:0.0.48" @@ -2315,6 +2353,15 @@ __metadata: languageName: node linkType: hard +"@types/es-aggregate-error@npm:^1.0.2": + version: 1.0.2 + resolution: "@types/es-aggregate-error@npm:1.0.2" + dependencies: + "@types/node": "*" + checksum: 076fd59b595f33c8c7e7eb68ec55bd43cf8b2cf7bbc6778e25d7ae1a5fa0538a0a56f149015f403d7bbcefe59f1d8182351685b59c1fe719fd46d0dd8a9737fa + languageName: node + linkType: hard + "@types/estree@npm:*": version: 0.0.50 resolution: "@types/estree@npm:0.0.50" @@ -2835,14 +2882,14 @@ __metadata: linkType: hard "ajv@npm:^8.0.0, ajv@npm:^8.0.1, ajv@npm:^8.6.0, ajv@npm:^8.6.3, ajv@npm:^8.8.2": - version: 8.8.2 - resolution: "ajv@npm:8.8.2" + version: 8.9.0 + resolution: "ajv@npm:8.9.0" dependencies: fast-deep-equal: ^3.1.1 json-schema-traverse: ^1.0.0 require-from-string: ^2.0.2 uri-js: ^4.2.2 - checksum: 90849ef03c4f4f7051d15f655120137b89e3205537d683beebd39d95f40c0ca00ea8476cd999602d2f433863e7e4bf1b81d1869d1e07f4dcf56d71b6430a605c + checksum: 756c048bfa917b43bb84c8a0a53e6a489123203bc4bdec8cbeb8ec2d715674f5e61d49560a1a6ec83268af4f33bed324f5cb6d9c76d96849fd58ed7089b8e7f3 languageName: node linkType: hard @@ -3058,7 +3105,16 @@ __metadata: languageName: node linkType: hard -"astring@npm:^1.7.5, astring@npm:^1.8.1": +"astring@npm:^1.7.5": + version: 1.7.5 + resolution: "astring@npm:1.7.5" + bin: + astring: bin/astring + checksum: b432c3f35473ccb7ae715c9e7f5f2ea3eb6864809396d5a0cd4a00959bced3d8aaddf3d87a86e5c0f2f00a210fe789c2835e9dc48ac7c6dfe8be483f655a9490 + languageName: node + linkType: hard + +"astring@npm:^1.8.1": version: 1.8.1 resolution: "astring@npm:1.8.1" bin: @@ -4642,6 +4698,20 @@ __metadata: languageName: node linkType: hard +"es-aggregate-error@npm:^1.0.7": + version: 1.0.7 + resolution: "es-aggregate-error@npm:1.0.7" + dependencies: + define-properties: ^1.1.3 + es-abstract: ^1.19.0 + function-bind: ^1.1.1 + functions-have-names: ^1.2.2 + get-intrinsic: ^1.1.1 + globalthis: ^1.0.2 + checksum: 16b89fefdf56c0478cd21577249156cf83e44c2220c057cbfddd99c01e15e03d6d90a85ce73dece4728a5bfcb022dc160e04a66b1f83a620f140842c6f8325f9 + languageName: node + linkType: hard + "es-to-primitive@npm:^1.2.1": version: 1.2.1 resolution: "es-to-primitive@npm:1.2.1" @@ -5457,6 +5527,13 @@ __metadata: languageName: node linkType: hard +"functions-have-names@npm:^1.2.2": + version: 1.2.2 + resolution: "functions-have-names@npm:1.2.2" + checksum: 25f44b6d1c41ac86ffdf41f25d1de81c0a5b4a3fcf4307a33cdfb23b9d4bd5d0d8bf312eaef5ad368c6500c8a9e19f692b8ce9f96aaab99db9dd936554165558 + languageName: node + linkType: hard + "gauge@npm:~2.7.3": version: 2.7.4 resolution: "gauge@npm:2.7.4" @@ -5631,6 +5708,15 @@ __metadata: languageName: node linkType: hard +"globalthis@npm:^1.0.2": + version: 1.0.2 + resolution: "globalthis@npm:1.0.2" + dependencies: + define-properties: ^1.1.3 + checksum: 5a5f3c7ab94708260a98106b35946b74bb57f6b2013e39668dc9e8770b80a3418103b63a2b4aa01c31af15fdf6a2940398ffc0a408573c34c2304f928895adff + languageName: node + linkType: hard + "globby@npm:^11.0.3": version: 11.0.4 resolution: "globby@npm:11.0.4" @@ -7009,7 +7095,14 @@ __metadata: languageName: node linkType: hard -"jsep@npm:^1.1.2, jsep@npm:^1.2.0": +"jsep@npm:^1.1.2": + version: 1.1.2 + resolution: "jsep@npm:1.1.2" + checksum: 189f29e522eec611793fcc81e8dbf919ecc1e9dd7858b9e34e32461280ae35e89a14ada91f8715b38650717b1571ab4480a46f53d60513079882ccc103f5b9c1 + languageName: node + linkType: hard + +"jsep@npm:^1.2.0": version: 1.2.0 resolution: "jsep@npm:1.2.0" checksum: 7166871e91f6be3409658b7417955a72bba4341b664e9cf062c9e0150e7f465aedc4d8fbb67e7967e3c224cbb41f155a0c16df489614421578cb96f1ba1c5519 @@ -7089,13 +7182,6 @@ __metadata: languageName: node linkType: hard -"json-schema@npm:0.4.0, json-schema@npm:^0.4.0": - version: 0.4.0 - resolution: "json-schema@npm:0.4.0" - checksum: 66389434c3469e698da0df2e7ac5a3281bcff75e797a5c127db7c5b56270e01ae13d9afa3c03344f76e32e81678337a8c912bdbb75101c62e487dc3778461d72 - languageName: node - linkType: hard - "json-stable-stringify-without-jsonify@npm:^1.0.1": version: 1.0.1 resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" @@ -9291,7 +9377,6 @@ __metadata: jest: ^27.4.3 jest-mock: ^27.4.2 jest-when: ^3.4.2 - json-schema: ^0.4.0 karma: ^6.1.1 karma-chrome-launcher: ^3.1.0 karma-jasmine: ^3.3.1