From bf5918c56b2cbe447b2acd0a8417bc7dc5a4eb4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Thu, 11 Jan 2024 14:50:09 +0100 Subject: [PATCH 1/6] feat(rulesets): improve {oas2,oas3}-valid-schema rule --- .eslintignore | 2 +- .gitignore | 1 + package.json | 3 +- packages/rulesets/package.json | 32 +- packages/rulesets/scripts/compile-schemas.ts | 93 +++ .../src/oas/__tests__/oas2-schema.test.ts | 32 + .../src/oas/__tests__/oas3-schema.test.ts | 332 ++++++-- .../__tests__/oasDocumentSchema.test.ts | 322 -------- .../oas/functions/__tests__/oasSchema.test.ts | 47 +- .../src/oas/functions/_oasDocumentSchema.ts | 116 +++ .../src/oas/functions/oasDocumentSchema.ts | 91 +- packages/rulesets/src/oas/index.ts | 2 + packages/rulesets/src/oas/schemas/LICENSE | 201 +++++ .../src/oas/schemas/json-schema-draft-04.json | 137 +++ .../rulesets/src/oas/schemas/oas/README.md | 1 + .../oas/schemas/{2.0.json => oas/v2.0.json} | 26 +- .../oas/schemas/{3.0.json => oas/v3.0.json} | 779 +++++++++++------- .../oas/schemas/oas/v3.1/dialect.schema.json | 25 + .../schemas/{3.1.json => oas/v3.1/index.json} | 274 +++--- .../src/oas/schemas/oas/v3.1/meta.schema.json | 87 ++ test-harness/scenarios/oas3-schema.scenario | 21 +- .../scenarios/oas3.1/petstore.scenario | 3 +- yarn.lock | 86 +- 23 files changed, 1727 insertions(+), 986 deletions(-) create mode 100644 packages/rulesets/scripts/compile-schemas.ts delete mode 100644 packages/rulesets/src/oas/functions/__tests__/oasDocumentSchema.test.ts create mode 100644 packages/rulesets/src/oas/functions/_oasDocumentSchema.ts create mode 100644 packages/rulesets/src/oas/schemas/LICENSE create mode 100644 packages/rulesets/src/oas/schemas/json-schema-draft-04.json create mode 100644 packages/rulesets/src/oas/schemas/oas/README.md rename packages/rulesets/src/oas/schemas/{2.0.json => oas/v2.0.json} (98%) rename packages/rulesets/src/oas/schemas/{3.0.json => oas/v3.0.json} (73%) create mode 100644 packages/rulesets/src/oas/schemas/oas/v3.1/dialect.schema.json rename packages/rulesets/src/oas/schemas/{3.1.json => oas/v3.1/index.json} (82%) create mode 100644 packages/rulesets/src/oas/schemas/oas/v3.1/meta.schema.json diff --git a/.eslintignore b/.eslintignore index a4962f609..f12606a84 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,6 @@ **/__fixtures__/** -/test-harness/**/*.yaml /test-harness/tests/ /packages/*/dist +/packages/rulesets/src/oas/schemas/compiled.ts /packages/*/CHANGELOG.md packages/formatters/src/html/templates.ts diff --git a/.gitignore b/.gitignore index c8bc0c569..69365a2d2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules !.yarn/versions packages/formatters/src/html/templates.ts +packages/rulesets/src/oas/schemas/compiled.ts packages/cli/binaries packages/*/src/version.ts /test-harness/tmp/ diff --git a/package.json b/package.json index ec485aa4b..e93a3febb 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "node": "^12.20 || >= 14.13" }, "scripts": { - "clean": "rimraf .cache packages/*/{dist,.cache}", + "preclean": "yarn workspaces foreach run preclean", + "clean": "yarn preclean && rimraf .cache packages/*/{dist,.cache}", "prebuild": "yarn workspaces foreach run prebuild", "build": "yarn prebuild && tsc --build ./tsconfig.build.json && yarn postbuild", "postbuild": "yarn workspaces foreach run postbuild", diff --git a/packages/rulesets/package.json b/packages/rulesets/package.json index 7ddcfe8d6..8f8483227 100644 --- a/packages/rulesets/package.json +++ b/packages/rulesets/package.json @@ -8,11 +8,26 @@ "node": ">=12" }, "license": "Apache-2.0", - "main": "dist/index.js", - "types": "dist/index.d.ts", "files": [ "/dist" ], + "type": "commonjs", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./oas": { + "types": "./dist/oas/index.d.ts", + "default": "./dist/oas/index.js" + }, + "./asyncapi": { + "types": "./dist/asyncapi/index.d.ts", + "default": "./dist/asyncapi/index.js" + } + }, "repository": { "type": "git", "url": "https://github.com/stoplightio/spectral.git" @@ -27,9 +42,10 @@ "@stoplight/spectral-runtime": "^1.1.1", "@stoplight/types": "^13.6.0", "@types/json-schema": "^7.0.7", - "ajv": "^8.8.2", + "ajv": "^8.12.0", "ajv-formats": "~2.1.0", "json-schema-traverse": "^1.0.0", + "leven": "3.1.0", "lodash": "~4.17.21", "tslib": "^2.3.0" }, @@ -37,6 +53,14 @@ "@stoplight/path": "^1.3.2", "@stoplight/spectral-parsers": "*", "@stoplight/spectral-ref-resolver": "*", - "immer": "^9.0.6" + "gzip-size": "^6.0.0", + "immer": "^9.0.6", + "terser": "^5.26.0" + }, + "scripts": { + "compile-schemas": "ts-node -T ./scripts/compile-schemas.ts", + "prelint": "yarn compile-schemas --quiet", + "pretest": "yarn compile-schemas --quiet", + "prebuild": "yarn compile-schemas --quiet" } } diff --git a/packages/rulesets/scripts/compile-schemas.ts b/packages/rulesets/scripts/compile-schemas.ts new file mode 100644 index 000000000..e73aef3a9 --- /dev/null +++ b/packages/rulesets/scripts/compile-schemas.ts @@ -0,0 +1,93 @@ +/* eslint-disable no-console */ +import * as fs from 'fs'; +import * as path from 'path'; +import * as process from 'process'; +import Ajv2020 from 'ajv/dist/2020.js'; +import standaloneCode from 'ajv/dist/standalone/index.js'; +import ajvErrors from 'ajv-errors'; +import ajvFormats from 'ajv-formats'; +import chalk from 'chalk'; +import { minify } from 'terser'; +import { sync } from 'gzip-size'; + +const cwd = path.join(__dirname, '../src'); + +const schemas = [ + 'oas/schemas/json-schema-draft-04.json', + 'oas/schemas/oas/v2.0.json', + 'oas/schemas/oas/v3.0.json', + 'oas/schemas/oas/v3.1/dialect.schema.json', + 'oas/schemas/oas/v3.1/meta.schema.json', + 'oas/schemas/oas/v3.1/index.json', +].map(async schema => JSON.parse(await fs.promises.readFile(path.join(cwd, schema), 'utf8'))); + +const log = process.argv.includes('--quiet') + ? (): void => { + /* no-op */ + } + : console.log.bind(console); + +Promise.all(schemas) + .then(async schemas => { + const ajv = new Ajv2020({ + schemas, + allErrors: true, + messages: true, + strict: false, + inlineRefs: false, + formats: { + 'media-range': true, + }, + code: { + esm: true, + source: true, + optimize: 1, + }, + }); + + ajvFormats(ajv); + ajvErrors(ajv); + + const target = path.join(cwd, 'oas/schemas/compiled.ts'); + const basename = path.basename(target); + const code = standaloneCode(ajv, { + oas2_0: 'http://swagger.io/v2/schema.json', + oas3_0: 'https://spec.openapis.org/oas/3.0/schema/2019-04-02', + oas3_1: 'https://spec.openapis.org/oas/3.1/schema/2021-09-28', + }); + + const minified = ( + await minify(code, { + compress: { + passes: 2, + ecma: 2020, + }, + ecma: 2020, + module: true, + mangle: { + toplevel: true, + module: true, + }, + format: { + comments: false, + }, + }) + ).code!; + + log( + 'writing %s size is %dKB (original), %dKB (minified) %dKB (minified + gzipped)', + path.join(target, '..', basename), + Math.round((code.length / 1024) * 100) / 100, + Math.round((minified.length / 1024) * 100) / 100, + Math.round((sync(minified) / 1024) * 100) / 100, + ); + + await fs.promises.writeFile(path.join(target, '..', basename), ['// @ts-nocheck', minified].join('\n')); + }) + .then(() => { + log(chalk.green('Validators generated.')); + }) + .catch(e => { + console.error(chalk.red('Error generating validators %s'), e.message); + process.exit(1); + }); diff --git a/packages/rulesets/src/oas/__tests__/oas2-schema.test.ts b/packages/rulesets/src/oas/__tests__/oas2-schema.test.ts index 97b28290e..8f6a4c796 100644 --- a/packages/rulesets/src/oas/__tests__/oas2-schema.test.ts +++ b/packages/rulesets/src/oas/__tests__/oas2-schema.test.ts @@ -25,4 +25,36 @@ testRule('oas2-schema', [ }, ], }, + + { + name: 'validate security definitions', + document: { + swagger: '2.0', + info: { + title: 'response example', + version: '1.0', + }, + paths: { + '/user': { + get: { + responses: { + 200: { + description: 'dummy description', + }, + }, + }, + }, + }, + securityDefinitions: { + basic: null, + }, + }, + errors: [ + { + message: 'Invalid basic authentication security definition.', + path: ['securityDefinitions', 'basic'], + severity: DiagnosticSeverity.Error, + }, + ], + }, ]); diff --git a/packages/rulesets/src/oas/__tests__/oas3-schema.test.ts b/packages/rulesets/src/oas/__tests__/oas3-schema.test.ts index e2cea76a5..cfbbe68dc 100644 --- a/packages/rulesets/src/oas/__tests__/oas3-schema.test.ts +++ b/packages/rulesets/src/oas/__tests__/oas3-schema.test.ts @@ -4,102 +4,110 @@ import testRule from '../../__tests__/__helpers__/tester'; testRule('oas3-schema', [ { name: 'human-readable Ajv errors', - document: require('./__fixtures__/petstore.invalid-schema.oas3.json'), - errors: [ - { - message: '"email" property must match format "email".', - path: ['info', 'contact', 'email'], - }, - { - message: '"header-1" property must have required property "schema".', - path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'], - }, - { - message: 'Property "type" is not expected to be here.', - path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1', 'type'], - }, - { - message: 'Property "op" is not expected to be here.', - path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1', 'op'], - }, - ], - }, - - { - name: 'sibling additionalProperties errors', document: { openapi: '3.0.0', info: { - title: 'Siblings', - version: '1.0', + version: '1.0.0', + title: 'Swagger Petstore', + license: { + name: 'MIT', + }, + contact: { + email: 'bar@foo', + }, + description: 'test', }, servers: [ { url: 'http://petstore.swagger.io/v1', }, ], - tags: [ - { - name: 'pets', - }, - ], paths: { '/pets': { - post: { - description: 'Add a new pet to the store', - summary: 'Create pet', - operationId: 'create_pet', + get: { + summary: 'List all pets', + operationId: 'listPets', + description: 'test', tags: ['pets'], - requestBody: { - description: 'Pet object that needs to be added to the store', - required: true, - content: { - 'application/json': { - schema: { - type: 'object', - }, + parameters: [ + { + name: 'limit', + in: 'query', + description: 'How many items to return at one time (max 100)', + required: false, + schema: { + type: 'integer', + format: 'int32', }, }, - }, + ], responses: { - '204': { - description: 'Success', - }, - '400': { - description: 'Bad request', - }, - '42': { - description: 'The answer to life, the universe, and everything', - }, - '9999': { - description: 'Four digits must be better than three', - }, - '5xx': { - description: 'Sumpin bad happened', + '200': { + description: 'A paged array of pets', + headers: { + 'x-next': { + description: 'A link to the next page of responses', + schema: { + type: 'string', + }, + }, + 'header-1': { + type: 'string', + op: 'foo', + }, + }, + content: { + 'application/json': { + schema: { + $ref: './models/pet.yaml', + }, + }, + }, }, default: { - description: 'Error', + description: 'unexpected error', + content: { + 'application/json': { + schema: { + $ref: '../common/models/error.yaml', + }, + }, + }, }, }, }, }, }, + components: { + schemas: { + Pets: { + type: 'array', + items: { + $ref: './models/pet.yaml', + }, + 'x-tags': ['Pets'], + title: 'Pets', + description: 'A list of pets.', + }, + }, + }, }, errors: [ { - message: 'Property "42" is not expected to be here.', - path: ['paths', '/pets', 'post', 'responses', '42'], - severity: DiagnosticSeverity.Error, + message: '"email" property must match format "email".', + path: ['info', 'contact', 'email'], }, { - message: 'Property "9999" is not expected to be here.', - path: ['paths', '/pets', 'post', 'responses', '9999'], - severity: DiagnosticSeverity.Error, + message: '"schema" or "content" must be present.', + path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'], }, { - message: 'Property "5xx" is not expected to be here.', - path: ['paths', '/pets', 'post', 'responses', '5xx'], - severity: DiagnosticSeverity.Error, + message: 'Property "type" is not expected to be here.', + path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1', 'type'], + }, + { + message: 'Property "op" is not expected to be here.', + path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1', 'op'], }, ], }, @@ -117,7 +125,7 @@ testRule('oas3-schema', [ }, errors: [ { - message: '"jsonSchemaDialect" property type must be string.', + message: '"jsonSchemaDialect" property must be string.', path: ['jsonSchemaDialect'], severity: DiagnosticSeverity.Error, }, @@ -187,4 +195,192 @@ testRule('oas3-schema', [ }, errors: [], }, + + { + name: 'oas3.0: validate parameters', + document: { + openapi: '3.0.1', + info: { + title: 'response example', + version: '1.0', + }, + paths: { + '/user': { + get: { + responses: { + 200: { + description: 'dummy description', + }, + }, + parameters: [ + { + name: 'cookie', + in: ' cookie', + required: true, + schema: { + type: ['string', 'number'], + }, + }, + { + name: 'module_id', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'size', + in: 'query', + required: true, + schema: { + type: 'numbers', + }, + }, + ], + }, + }, + }, + }, + errors: [ + { + message: + '"in" property must be equal to one of the allowed values: "path", "query", "header", "cookie". Did you mean "cookie"?.', + path: ['paths', '/user', 'get', 'parameters', '0', 'in'], + severity: DiagnosticSeverity.Error, + }, + { + message: + '"type" property must be equal to one of the allowed values: "array", "boolean", "integer", "number", "object", "string".', + path: ['paths', '/user', 'get', 'parameters', '0', 'schema', 'type'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Parameter must have a valid "in" property.', + path: ['paths', '/user', 'get', 'parameters', '1'], + severity: DiagnosticSeverity.Error, + }, + { + message: + '"type" property must be equal to one of the allowed values: "array", "boolean", "integer", "number", "object", "string". Did you mean "number"?.', + path: ['paths', '/user', 'get', 'parameters', '2', 'schema', 'type'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'oas3.0: validate security schemes', + document: { + openapi: '3.0.1', + info: { + title: 'response example', + version: '1.0', + }, + paths: { + '/user': { + get: { + responses: { + 200: { + description: 'dummy description', + }, + }, + }, + }, + }, + components: { + securitySchemes: { + basic: { + foo: 2, + }, + http: { + type: 'https', + scheme: 'basic', + }, + apiKey: null, + }, + }, + }, + errors: [ + { + message: 'Security scheme must have a valid type.', + path: ['components', 'securitySchemes', 'basic'], + severity: DiagnosticSeverity.Error, + }, + { + message: + '"type" property must be equal to one of the allowed values: "apiKey", "http", "oauth2", "openIdConnect". Did you mean "http"?.', + path: ['components', 'securitySchemes', 'http', 'type'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Invalid security scheme.', + path: ['components', 'securitySchemes', 'apiKey'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + { + name: 'oas3.0: validate responses', + document: { + openapi: '3.0.1', + info: { + title: 'response example', + version: '1.0', + }, + paths: { + '/user': { + get: { + operationId: 'd', + responses: { + 200: {}, + }, + }, + post: { + responses: { + '204': { + description: 'Success', + }, + '400': { + description: 'Bad request', + }, + '42': { + description: 'The answer to life, the universe, and everything', + }, + '9999': { + description: 'Four digits must be better than three', + }, + '5xx': { + description: 'Sumpin bad happened', + }, + default: { + description: 'Error', + }, + }, + }, + }, + }, + }, + errors: [ + { + message: '"200" property must have required property "description".', + path: ['paths', '/user', 'get', 'responses', '200'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Property "42" is not expected to be here.', + path: ['paths', '/user', 'post', 'responses', '42'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Property "9999" is not expected to be here.', + path: ['paths', '/user', 'post', 'responses', '9999'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Property "5xx" is not expected to be here.', + path: ['paths', '/user', 'post', 'responses', '5xx'], + severity: DiagnosticSeverity.Error, + }, + ], + }, ]); diff --git a/packages/rulesets/src/oas/functions/__tests__/oasDocumentSchema.test.ts b/packages/rulesets/src/oas/functions/__tests__/oasDocumentSchema.test.ts deleted file mode 100644 index 0f9d33914..000000000 --- a/packages/rulesets/src/oas/functions/__tests__/oasDocumentSchema.test.ts +++ /dev/null @@ -1,322 +0,0 @@ -import { DiagnosticSeverity } from '@stoplight/types'; -import { Spectral } from '@stoplight/spectral-core'; -import { prepareResults } from '../oasDocumentSchema'; - -import { ErrorObject } from 'ajv'; -import { createWithRules } from '../../__tests__/__helpers__/tester'; - -describe('oasDocumentSchema', () => { - let s: Spectral; - - beforeEach(async () => { - s = createWithRules(['oas2-schema', 'oas3-schema']); - }); - - describe('given OpenAPI 2 document', () => { - test('validate security definitions', async () => { - expect( - await s.run({ - swagger: '2.0', - info: { - title: 'response example', - version: '1.0', - }, - paths: { - '/user': { - get: { - responses: { - 200: { - description: 'dummy description', - }, - }, - }, - }, - }, - securityDefinitions: { - basic: null, - }, - }), - ).toEqual([ - { - code: 'oas2-schema', - message: 'Invalid basic authentication security definition.', - path: ['securityDefinitions', 'basic'], - severity: DiagnosticSeverity.Error, - range: expect.any(Object), - }, - ]); - }); - }); - - describe('given OpenAPI 3 document', () => { - test('validate parameters', async () => { - expect( - await s.run({ - openapi: '3.0.1', - info: { - title: 'response example', - version: '1.0', - }, - paths: { - '/user': { - get: { - responses: { - 200: { - description: 'dummy description', - }, - }, - parameters: [ - { - name: 'module_id', - in: 'bar', - required: true, - schema: { - type: ['string', 'number'], - }, - }, - ], - }, - }, - }, - }), - ).toEqual([ - { - code: 'oas3-schema', - message: '"type" property type must be string.', - path: ['paths', '/user', 'get', 'parameters', '0', 'schema', 'type'], - severity: DiagnosticSeverity.Error, - range: expect.any(Object), - }, - ]); - }); - - test('validate security schemes', async () => { - expect( - await s.run({ - openapi: '3.0.1', - info: { - title: 'response example', - version: '1.0', - }, - paths: { - '/user': { - get: { - responses: { - 200: { - description: 'dummy description', - }, - }, - }, - }, - }, - components: { - securitySchemes: { - basic: { - foo: 2, - }, - }, - }, - }), - ).toEqual([ - { - code: 'oas3-schema', - message: 'Invalid security scheme.', - path: ['components', 'securitySchemes', 'basic'], - severity: DiagnosticSeverity.Error, - range: expect.any(Object), - }, - { - code: 'oas3-schema', - message: 'Property "foo" is not expected to be here.', - path: ['components', 'securitySchemes', 'basic', 'foo'], - severity: DiagnosticSeverity.Error, - range: expect.any(Object), - }, - ]); - }); - - test('validate responses', async () => { - expect( - await s.run({ - openapi: '3.0.1', - info: { - title: 'response example', - version: '1.0', - }, - paths: { - '/user': { - get: { - operationId: 'd', - responses: { - 200: {}, - }, - }, - }, - }, - }), - ).toEqual([ - { - code: 'oas3-schema', - message: '"200" property must have required property "description".', - path: ['paths', '/user', 'get', 'responses', '200'], - severity: DiagnosticSeverity.Error, - range: expect.any(Object), - }, - ]); - }); - }); - - describe('prepareResults', () => { - test('given oneOf error one of which is required $ref property missing, picks only one error', () => { - const errors: ErrorObject[] = [ - { - keyword: 'type', - instancePath: '/paths/test/post/parameters/0/schema/type', - schemaPath: '#/properties/type/type', - params: { type: 'string' }, - message: 'must be string', - }, - { - keyword: 'required', - instancePath: '/paths/test/post/parameters/0/schema', - schemaPath: '#/definitions/Reference/required', - params: { missingProperty: '$ref' }, - message: "must have required property '$ref'", - }, - { - keyword: 'oneOf', - instancePath: '/paths/test/post/parameters/0/schema', - schemaPath: '#/properties/schema/oneOf', - params: { passingSchemas: null }, - message: 'must match exactly one schema in oneOf', - }, - ]; - - prepareResults(errors); - - expect(errors).toStrictEqual([ - { - keyword: 'type', - instancePath: '/paths/test/post/parameters/0/schema/type', - schemaPath: '#/properties/type/type', - params: { type: 'string' }, - message: 'must be string', - }, - ]); - }); - - test('given oneOf error one without any $ref property missing, picks all errors', () => { - const errors: ErrorObject[] = [ - { - keyword: 'type', - instancePath: '/paths/test/post/parameters/0/schema/type', - schemaPath: '#/properties/type/type', - params: { type: 'string' }, - message: 'must be string', - }, - { - keyword: 'type', - instancePath: '/paths/test/post/parameters/1/schema/type', - schemaPath: '#/properties/type/type', - params: { type: 'string' }, - message: 'must be string', - }, - { - keyword: 'oneOf', - instancePath: '/paths/test/post/parameters/0/schema', - schemaPath: '#/properties/schema/oneOf', - params: { passingSchemas: null }, - message: 'must match exactly one schema in oneOf', - }, - ]; - - prepareResults(errors); - - expect(errors).toStrictEqual([ - { - keyword: 'type', - instancePath: '/paths/test/post/parameters/0/schema/type', - schemaPath: '#/properties/type/type', - params: { type: 'string' }, - message: 'must be string', - }, - { - instancePath: '/paths/test/post/parameters/1/schema/type', - keyword: 'type', - message: 'must be string', - params: { - type: 'string', - }, - schemaPath: '#/properties/type/type', - }, - { - instancePath: '/paths/test/post/parameters/0/schema', - keyword: 'oneOf', - message: 'must match exactly one schema in oneOf', - params: { - passingSchemas: null, - }, - schemaPath: '#/properties/schema/oneOf', - }, - ]); - }); - - test('given errors with different data paths, picks all errors', () => { - const errors: ErrorObject[] = [ - { - keyword: 'type', - instancePath: '/paths/test/post/parameters/0/schema/type', - schemaPath: '#/properties/type/type', - params: { type: 'string' }, - message: 'must be string', - }, - { - keyword: 'required', - instancePath: '/paths/foo/post/parameters/0/schema', - schemaPath: '#/definitions/Reference/required', - params: { missingProperty: '$ref' }, - message: "must have required property '$ref'", - }, - { - keyword: 'oneOf', - instancePath: '/paths/baz/post/parameters/0/schema', - schemaPath: '#/properties/schema/oneOf', - params: { passingSchemas: null }, - message: 'must match exactly one schema in oneOf', - }, - ]; - - prepareResults(errors); - - expect(errors).toStrictEqual([ - { - instancePath: '/paths/test/post/parameters/0/schema/type', - keyword: 'type', - message: 'must be string', - params: { - type: 'string', - }, - schemaPath: '#/properties/type/type', - }, - { - instancePath: '/paths/foo/post/parameters/0/schema', - keyword: 'required', - message: "must have required property '$ref'", - params: { - missingProperty: '$ref', - }, - schemaPath: '#/definitions/Reference/required', - }, - { - instancePath: '/paths/baz/post/parameters/0/schema', - keyword: 'oneOf', - message: 'must match exactly one schema in oneOf', - params: { - passingSchemas: null, - }, - schemaPath: '#/properties/schema/oneOf', - }, - ]); - }); - }); -}); diff --git a/packages/rulesets/src/oas/functions/__tests__/oasSchema.test.ts b/packages/rulesets/src/oas/functions/__tests__/oasSchema.test.ts index f9cc46926..19a86b4d2 100644 --- a/packages/rulesets/src/oas/functions/__tests__/oasSchema.test.ts +++ b/packages/rulesets/src/oas/functions/__tests__/oasSchema.test.ts @@ -1,8 +1,8 @@ +import type { RulesetFunctionContext } from '@stoplight/spectral-core'; import { oas2, oas3, oas3_0, oas3_1 } from '@stoplight/spectral-formats'; -import { DeepPartial } from '@stoplight/types'; +import type { DeepPartial } from '@stoplight/types'; + import oasSchema from '../../functions/oasSchema'; -import { createWithRules } from '../../__tests__/__helpers__/tester'; -import { RulesetFunctionContext } from '@stoplight/spectral-core/src'; function runSchema(target: unknown, schemaObj: Record, context?: DeepPartial) { return oasSchema(target, { schema: schemaObj }, { @@ -153,45 +153,4 @@ describe('oasSchema', () => { expect(runSchema(1.5, testSchema, { document })).toEqual([]); }); - - test('should remove all redundant ajv errors', async () => { - const spectral = createWithRules(['oas3-schema', 'oas3-valid-schema-example', 'oas3-valid-media-example']); - const invalidSchema = JSON.stringify(require('../../__tests__/__fixtures__/petstore.invalid-schema.oas3.json')); - - const result = await spectral.run(invalidSchema); - - expect(result).toEqual([ - expect.objectContaining({ - code: 'oas3-schema', - message: '"email" property must match format "email".', - path: ['info', 'contact', 'email'], - }), - expect.objectContaining({ - code: 'oas3-schema', - message: '"header-1" property must have required property "schema".', - path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'], - }), - expect.objectContaining({ - code: 'oas3-schema', - message: 'Property "type" is not expected to be here.', - path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1', 'type'], - }), - expect.objectContaining({ - code: 'oas3-schema', - message: 'Property "op" is not expected to be here.', - path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1', 'op'], - }), - expect.objectContaining({ - code: 'invalid-ref', - }), - expect.objectContaining({ - code: 'invalid-ref', - }), - expect.objectContaining({ - code: 'oas3-valid-schema-example', - message: '"example" property type must be number', - path: ['components', 'schemas', 'foo', 'example'], - }), - ]); - }); }); diff --git a/packages/rulesets/src/oas/functions/_oasDocumentSchema.ts b/packages/rulesets/src/oas/functions/_oasDocumentSchema.ts new file mode 100644 index 000000000..68b2ea3f4 --- /dev/null +++ b/packages/rulesets/src/oas/functions/_oasDocumentSchema.ts @@ -0,0 +1,116 @@ +import type { IFunctionResult } from '@stoplight/spectral-core'; +import { isPlainObject, resolveInlineRef } from '@stoplight/json'; +import type { ErrorObject } from 'ajv'; +import leven from 'leven'; + +import { oas2_0, oas3_0, oas3_1 } from '../schemas/compiled'; + +function getValidator(format: 'oas2_0' | 'oas3_0' | 'oas3_1'): typeof oas2_0 | typeof oas3_0 | typeof oas3_1 { + switch (format) { + case 'oas2_0': + return oas2_0; + case 'oas3_0': + return oas3_0; + case 'oas3_1': + return oas3_1; + } +} + +function isRelevantError(error: ErrorObject): boolean { + return error.keyword !== 'if'; +} + +export default function (format: 'oas2_0' | 'oas3_0' | 'oas3_1', input: unknown): IFunctionResult[] | void { + const validator = getValidator(format); + validator(input); + + // @ts-expect-error: validator typings aren't fully correct + const errors = validator.errors as ErrorObject[] | undefined; + + return errors?.filter(isRelevantError).map(e => processError(input, e)); +} + +function processError(input: unknown, error: ErrorObject): IFunctionResult { + const path = error.instancePath === '' ? [] : error.instancePath.slice(1).split('/'); + const property = path.length === 0 ? null : path[path.length - 1]; + + switch (error.keyword) { + case 'additionalProperties': { + const additionalProperty = error.params['additionalProperty'] as string; + path.push(additionalProperty); + + return { + message: `Property "${additionalProperty}" is not expected to be here`, + path, + }; + } + + case 'enum': { + const allowedValues = error.params['allowedValues'] as unknown[]; + const printedValues = allowedValues.map(value => JSON.stringify(value)).join(', '); + let suggestion: string; + + if (!isPlainObject(input)) { + suggestion = ''; + } else { + const value = resolveInlineRef(input, `#${error.instancePath}`); + if (typeof value !== 'string') { + suggestion = ''; + } else { + const bestMatch = findBestMatch(value, allowedValues); + + if (bestMatch !== null) { + suggestion = `. Did you mean "${bestMatch}"?`; + } else { + suggestion = ''; + } + } + } + + return { + message: `${cleanAjvMessage(property, error.message)}: ${printedValues}${suggestion}`, + path, + }; + } + + case 'errorMessage': + return { + message: String(error.message), + path, + }; + + default: + return { + message: cleanAjvMessage(property, error.message), + path, + }; + } +} + +function findBestMatch(value: string, allowedValues: unknown[]): string | null { + const matches = allowedValues + .filter((value): value is string => typeof value === 'string') + .map(allowedValue => ({ + value: allowedValue, + weight: leven(value, allowedValue), + })) + .sort((x, y) => (x.weight > y.weight ? 1 : x.weight < y.weight ? -1 : 0)); + + if (matches.length === 0) { + return null; + } + + const bestMatch = matches[0]; + + return allowedValues.length === 1 || bestMatch.weight < bestMatch.value.length ? bestMatch.value : null; +} + +const QUOTES = /['"]/g; +const NOT = /NOT/g; + +function cleanAjvMessage(prop: string | null, message: string | undefined): string { + if (typeof message !== 'string') return ''; + + const cleanedMessage = message.replace(QUOTES, '"').replace(NOT, 'not'); + return prop === null ? cleanedMessage : `"${prop}" property ${cleanedMessage}`; +} diff --git a/packages/rulesets/src/oas/functions/oasDocumentSchema.ts b/packages/rulesets/src/oas/functions/oasDocumentSchema.ts index a2e731819..8c5397365 100644 --- a/packages/rulesets/src/oas/functions/oasDocumentSchema.ts +++ b/packages/rulesets/src/oas/functions/oasDocumentSchema.ts @@ -1,99 +1,18 @@ -import type { ErrorObject } from 'ajv'; -import { createRulesetFunction, IFunctionResult } from '@stoplight/spectral-core'; -import { schema as schemaFn } from '@stoplight/spectral-functions'; +import { createRulesetFunction } from '@stoplight/spectral-core'; import { oas2, oas3_1 } from '@stoplight/spectral-formats'; - -import * as schemaOas2_0 from '../schemas/2.0.json'; -import * as schemaOas3_0 from '../schemas/3.0.json'; -import * as schemaOas3_1 from '../schemas/3.1.json'; - -const OAS_SCHEMAS = { - '2.0': schemaOas2_0, - '3.0': schemaOas3_0, - '3.1': schemaOas3_1, -}; - -function shouldIgnoreError(error: ErrorObject): boolean { - return ( - // oneOf is a fairly error as we have 2 options to choose from for most of the time. - error.keyword === 'oneOf' || - // the required $ref is entirely useless, since oas-schema rules operate on resolved content, so there won't be any $refs in the document - (error.keyword === 'required' && error.params.missingProperty === '$ref') - ); -} - -// this is supposed to cover edge cases we need to cover manually, when it's impossible to detect the most appropriate error, i.e. oneOf consisting of more than 3 members, etc. -// note, more errors can be included if certain messages reported by AJV are not quite meaningful -const ERROR_MAP = [ - { - path: /^components\/securitySchemes\/[^/]+$/, - message: 'Invalid security scheme', - }, -]; - -// The function removes irrelevant (aka misleading, confusing, useless, whatever you call it) errors. -// There are a few exceptions, i.e. security components I covered manually, -// yet apart from them we usually deal with a relatively simple scenario that can be literally expressed as: "either proper value of $ref property". -// The $ref part is never going to be interesting for us, because both oas-schema rules operate on resolved content, so we won't have any $refs left. -// As you can see, what we deal here wit is actually not really oneOf anymore - it's always the first member of oneOf we match against. -// That being said, we always strip both oneOf and $ref, since we are always interested in the first error. -export function prepareResults(errors: ErrorObject[]): void { - // Update additionalProperties errors to make them more precise and prevent them from being treated as duplicates - for (const error of errors) { - if (error.keyword === 'additionalProperties') { - error.instancePath = `${error.instancePath}/${String(error.params['additionalProperty'])}`; - } - } - - for (let i = 0; i < errors.length; i++) { - const error = errors[i]; - - if (i + 1 < errors.length && errors[i + 1].instancePath === error.instancePath) { - errors.splice(i + 1, 1); - i--; - } else if (i > 0 && shouldIgnoreError(error) && errors[i - 1].instancePath.startsWith(error.instancePath)) { - errors.splice(i, 1); - i--; - } - } -} - -function applyManualReplacements(errors: IFunctionResult[]): void { - for (const error of errors) { - if (error.path === void 0) continue; - - const joinedPath = error.path.join('/'); - - for (const mappedError of ERROR_MAP) { - if (mappedError.path.test(joinedPath)) { - error.message = mappedError.message; - break; - } - } - } -} +import _oasDocumentSchema from './_oasDocumentSchema'; export default createRulesetFunction( { input: null, options: null, }, - function oasDocumentSchema(targetVal, opts, context) { + function oasDocumentSchema(input, _opts, context) { const formats = context.document.formats; if (formats === null || formats === void 0) return; - const schema = formats.has(oas2) - ? OAS_SCHEMAS['2.0'] - : formats.has(oas3_1) - ? OAS_SCHEMAS['3.1'] - : OAS_SCHEMAS['3.0']; - - const errors = schemaFn(targetVal, { allErrors: true, schema, prepareResults }, context); - - if (Array.isArray(errors)) { - applyManualReplacements(errors); - } + const format = formats.has(oas2) ? 'oas2_0' : formats.has(oas3_1) ? 'oas3_1' : 'oas3_0'; - return errors; + return _oasDocumentSchema(format, input); }, ); diff --git a/packages/rulesets/src/oas/index.ts b/packages/rulesets/src/oas/index.ts index f6f0e0516..09e208afd 100644 --- a/packages/rulesets/src/oas/index.ts +++ b/packages/rulesets/src/oas/index.ts @@ -522,6 +522,7 @@ const ruleset = { recommended: true, formats: [oas2], severity: 0, + resolved: false, given: '$', then: { function: oasDocumentSchema, @@ -678,6 +679,7 @@ const ruleset = { severity: 0, formats: [oas3], recommended: true, + resolved: false, given: '$', then: { function: oasDocumentSchema, diff --git a/packages/rulesets/src/oas/schemas/LICENSE b/packages/rulesets/src/oas/schemas/LICENSE new file mode 100644 index 000000000..23b34fdff --- /dev/null +++ b/packages/rulesets/src/oas/schemas/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright The Linux Foundation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/rulesets/src/oas/schemas/json-schema-draft-04.json b/packages/rulesets/src/oas/schemas/json-schema-draft-04.json new file mode 100644 index 000000000..e6ba2e95b --- /dev/null +++ b/packages/rulesets/src/oas/schemas/json-schema-draft-04.json @@ -0,0 +1,137 @@ +{ + "$id": "http://json-schema.org/draft-04/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "positiveInteger": { + "type": "integer", + "minimum": 0 + }, + "positiveIntegerDefault0": { + "allOf": [{ "$ref": "#/definitions/positiveInteger" }, { "default": 0 }] + }, + "simpleTypes": { + "enum": ["array", "boolean", "integer", "null", "number", "object", "string"] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "uniqueItems": true + } + }, + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uri" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "deprecationMessage": { + "type": "string", + "description": "Non-standard: deprecation message for a property, if it is deprecated" + }, + "default": {}, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "maxLength": { "$ref": "#/definitions/positiveInteger" }, + "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { + "anyOf": [{ "type": "boolean" }, { "$ref": "#" }], + "default": {} + }, + "items": { + "anyOf": [{ "$ref": "#" }, { "$ref": "#/definitions/schemaArray" }], + "default": {} + }, + "maxItems": { "$ref": "#/definitions/positiveInteger" }, + "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { "$ref": "#/definitions/positiveInteger" }, + "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { + "anyOf": [{ "type": "boolean" }, { "$ref": "#" }], + "default": {} + }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [{ "$ref": "#" }, { "$ref": "#/definitions/stringArray" }] + } + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": {} +} diff --git a/packages/rulesets/src/oas/schemas/oas/README.md b/packages/rulesets/src/oas/schemas/oas/README.md new file mode 100644 index 000000000..3ca934314 --- /dev/null +++ b/packages/rulesets/src/oas/schemas/oas/README.md @@ -0,0 +1 @@ +The schemas here are based on https://github.com/OAI/OpenAPI-Specification/blob/main/schemas/ with a few changes to yield more useful validation results. diff --git a/packages/rulesets/src/oas/schemas/2.0.json b/packages/rulesets/src/oas/schemas/oas/v2.0.json similarity index 98% rename from packages/rulesets/src/oas/schemas/2.0.json rename to packages/rulesets/src/oas/schemas/oas/v2.0.json index 7fa341242..3c0c73047 100644 --- a/packages/rulesets/src/oas/schemas/2.0.json +++ b/packages/rulesets/src/oas/schemas/oas/v2.0.json @@ -1,7 +1,7 @@ { "title": "A JSON Schema for Swagger 2.0 API.", "$id": "http://swagger.io/v2/schema.json#", - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": ["swagger", "info", "paths"], "additionalProperties": false, @@ -1029,7 +1029,6 @@ "$ref": "http://json-schema.org/draft-04/schema#/definitions/stringArray" }, "type": { - "type": "string", "enum": ["file"] }, "readOnly": { @@ -1048,7 +1047,6 @@ "additionalProperties": false, "properties": { "type": { - "type": "string", "enum": ["string", "number", "integer", "boolean", "array"] }, "format": { @@ -1211,7 +1209,6 @@ "required": ["type"], "properties": { "type": { - "type": "string", "enum": ["basic"] }, "description": { @@ -1230,14 +1227,12 @@ "required": ["type", "name", "in"], "properties": { "type": { - "type": "string", "enum": ["apiKey"] }, "name": { "type": "string" }, "in": { - "type": "string", "enum": ["header", "query"] }, "description": { @@ -1256,11 +1251,9 @@ "required": ["type", "flow", "authorizationUrl", "scopes"], "properties": { "type": { - "type": "string", "enum": ["oauth2"] }, "flow": { - "type": "string", "enum": ["implicit"] }, "scopes": { @@ -1286,11 +1279,9 @@ "required": ["type", "flow", "tokenUrl", "scopes"], "properties": { "type": { - "type": "string", "enum": ["oauth2"] }, "flow": { - "type": "string", "enum": ["password"] }, "scopes": { @@ -1316,12 +1307,10 @@ "required": ["type", "flow", "tokenUrl", "scopes"], "properties": { "type": { - "type": "string", - "enum": ["oauth2"] + "const": "oauth2" }, "flow": { - "type": "string", - "enum": ["application"] + "const": "application" }, "scopes": { "$ref": "#/definitions/oauth2Scopes" @@ -1346,12 +1335,10 @@ "required": ["type", "flow", "authorizationUrl", "tokenUrl", "scopes"], "properties": { "type": { - "type": "string", - "enum": ["oauth2"] + "const": "oauth2" }, "flow": { - "type": "string", - "enum": ["accessCode"] + "const": "accessCode" }, "scopes": { "$ref": "#/definitions/oauth2Scopes" @@ -1407,18 +1394,15 @@ "type": "array", "description": "The transfer protocol of the API.", "items": { - "type": "string", "enum": ["http", "https", "ws", "wss"] }, "uniqueItems": true }, "collectionFormat": { - "type": "string", "enum": ["csv", "ssv", "tsv", "pipes"], "default": "csv" }, "collectionFormatWithMulti": { - "type": "string", "enum": ["csv", "ssv", "tsv", "pipes", "multi"], "default": "csv" }, diff --git a/packages/rulesets/src/oas/schemas/3.0.json b/packages/rulesets/src/oas/schemas/oas/v3.0.json similarity index 73% rename from packages/rulesets/src/oas/schemas/3.0.json rename to packages/rulesets/src/oas/schemas/oas/v3.0.json index 3e500737d..867ee6e48 100644 --- a/packages/rulesets/src/oas/schemas/3.0.json +++ b/packages/rulesets/src/oas/schemas/oas/v3.0.json @@ -1,6 +1,6 @@ { "$id": "https://spec.openapis.org/oas/3.0/schema/2019-04-02", - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "description": "Validation schema for OpenAPI Specification 3.0.X.", "type": "object", "required": ["openapi", "info", "paths"], @@ -173,14 +173,16 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } } } }, @@ -188,14 +190,16 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/Response" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Response" + } } } }, @@ -203,14 +207,16 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/Parameter" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Parameter" + } } } }, @@ -218,14 +224,16 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/Example" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Example" + } } } }, @@ -233,14 +241,16 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/RequestBody" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/RequestBody" + } } } }, @@ -248,14 +258,16 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/Header" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Header" + } } } }, @@ -263,14 +275,16 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/SecurityScheme" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/SecurityScheme" + } } } }, @@ -278,14 +292,16 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/Link" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Link" + } } } }, @@ -293,14 +309,16 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/Callback" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Callback" + } } } } @@ -384,93 +402,110 @@ "uniqueItems": false }, "type": { - "type": "string", "enum": ["array", "boolean", "integer", "number", "object", "string"] }, "not": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } }, "allOf": { "type": "array", "items": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } } }, "oneOf": { "type": "array", "items": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } } }, "anyOf": { "type": "array", "items": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - } - ] - } - }, - "items": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" + "if": { + "type": "object", + "required": ["$ref"] }, - { + "then": { "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" } - ] + } + }, + "items": { + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } }, "properties": { "type": "object", "additionalProperties": { + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } + } + }, + "additionalProperties": { + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { "oneOf": [ { "$ref": "#/definitions/Schema" }, { - "$ref": "#/definitions/Reference" + "type": "boolean" } ] - } - }, - "additionalProperties": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - }, - { - "type": "boolean" - } - ], + }, "default": true }, "description": { @@ -564,14 +599,16 @@ "headers": { "type": "object", "additionalProperties": { - "oneOf": [ - { - "$ref": "#/definitions/Header" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Header" + } } }, "content": { @@ -583,14 +620,16 @@ "links": { "type": "object", "additionalProperties": { - "oneOf": [ - { - "$ref": "#/definitions/Link" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Link" + } } } }, @@ -603,27 +642,31 @@ "type": "object", "properties": { "schema": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } }, "example": {}, "examples": { "type": "object", "additionalProperties": { - "oneOf": [ - { - "$ref": "#/definitions/Example" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Example" + } } }, "encoding": { @@ -682,8 +725,7 @@ "default": false }, "style": { - "type": "string", - "enum": ["simple"], + "const": "simple", "default": "simple" }, "explode": { @@ -694,14 +736,16 @@ "default": false }, "schema": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } }, "content": { "type": "object", @@ -715,14 +759,16 @@ "examples": { "type": "object", "additionalProperties": { - "oneOf": [ - { - "$ref": "#/definitions/Example" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Example" + } } } }, @@ -770,14 +816,16 @@ "parameters": { "type": "array", "items": { - "oneOf": [ - { - "$ref": "#/definitions/Parameter" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Parameter" + } }, "uniqueItems": true } @@ -815,26 +863,30 @@ "parameters": { "type": "array", "items": { - "oneOf": [ - { - "$ref": "#/definitions/Parameter" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Parameter" + } }, "uniqueItems": true }, "requestBody": { - "oneOf": [ - { - "$ref": "#/definitions/RequestBody" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/RequestBody" + } }, "responses": { "$ref": "#/definitions/Responses" @@ -842,14 +894,16 @@ "callbacks": { "type": "object", "additionalProperties": { - "oneOf": [ - { - "$ref": "#/definitions/Callback" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Callback" + } } }, "deprecated": { @@ -878,26 +932,30 @@ "type": "object", "properties": { "default": { - "oneOf": [ - { - "$ref": "#/definitions/Response" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Response" + } } }, "patternProperties": { "^[1-5](?:\\d{2}|XX)$": { - "oneOf": [ - { - "$ref": "#/definitions/Response" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Response" + } }, "^x-": {} }, @@ -957,15 +1015,23 @@ }, "SchemaXORContent": { "description": "Schema and content are mutually exclusive, at least one is required", + "errorMessage": { + "not": "Schema and content are mutually exclusive, at least one is required" + }, "not": { "required": ["schema", "content"] }, - "oneOf": [ - { - "required": ["schema"] + "if": { + "type": "object", + "required": ["schema"] + }, + "then": true, + "else": { + "if": { + "type": "object", + "required": ["content"] }, - { - "required": ["content"], + "then": { "description": "Some properties are not allowed if content is present", "allOf": [ { @@ -994,8 +1060,14 @@ } } ] + }, + "else": { + "not": true, + "errorMessage": { + "not": "\"schema\" or \"content\" must be present" + } } - ] + } }, "Parameter": { "type": "object", @@ -1032,14 +1104,16 @@ "default": false }, "schema": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } }, "content": { "type": "object", @@ -1053,14 +1127,16 @@ "examples": { "type": "object", "additionalProperties": { - "oneOf": [ - { - "$ref": "#/definitions/Example" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Example" + } } } }, @@ -1068,7 +1144,7 @@ "^x-": {} }, "additionalProperties": false, - "required": ["name", "in"], + "required": ["name"], "allOf": [ { "$ref": "#/definitions/ExampleXORExamples" @@ -1083,60 +1159,101 @@ }, "ParameterLocation": { "description": "Parameter location", - "oneOf": [ - { - "description": "Parameter in path", - "required": ["required"], + "type": "object", + "if": { + "type": "object", + "properties": { + "in": { + "const": "path" + } + }, + "required": ["in"] + }, + "then": { + "description": "Parameter in path", + "required": ["required"], + "properties": { + "style": { + "enum": ["matrix", "label", "simple"], + "default": "simple" + }, + "required": { + "const": true + } + } + }, + "else": { + "if": { + "type": "object", "properties": { "in": { - "enum": ["path"] - }, - "style": { - "enum": ["matrix", "label", "simple"], - "default": "simple" - }, - "required": { - "enum": [true] + "const": "query" } - } + }, + "required": ["in"] }, - { + "then": { "description": "Parameter in query", "properties": { - "in": { - "enum": ["query"] - }, "style": { "enum": ["form", "spaceDelimited", "pipeDelimited", "deepObject"], "default": "form" } } }, - { - "description": "Parameter in header", - "properties": { - "in": { - "enum": ["header"] + "else": { + "if": { + "type": "object", + "properties": { + "in": { + "const": "header" + } }, - "style": { - "enum": ["simple"], - "default": "simple" + "required": ["in"] + }, + "then": { + "description": "Parameter in header", + "properties": { + "style": { + "const": "simple", + "default": "simple" + } } - } - }, - { - "description": "Parameter in cookie", - "properties": { - "in": { - "enum": ["cookie"] + }, + "else": { + "if": { + "type": "object", + "properties": { + "in": { + "const": "cookie" + } + }, + "required": ["in"] }, - "style": { - "enum": ["form"], - "default": "form" + "then": { + "description": "Parameter in cookie", + "properties": { + "style": { + "const": "form", + "default": "form" + } + } + }, + "else": { + "type": "object", + "properties": { + "in": { + "enum": ["path", "query", "header", "cookie"] + } + }, + "required": ["in"], + "errorMessage": { + "required": "Parameter must have a valid \"in\" property" + } } } } - ] + } }, "RequestBody": { "type": "object", @@ -1162,34 +1279,85 @@ "additionalProperties": false }, "SecurityScheme": { - "oneOf": [ - { - "$ref": "#/definitions/APIKeySecurityScheme" + "if": { + "type": "object", + "properties": { + "type": { + "const": "apiKey" + } }, - { - "$ref": "#/definitions/HTTPSecurityScheme" + "required": ["type"] + }, + "then": { + "$ref": "#/definitions/APIKeySecurityScheme" + }, + "else": { + "if": { + "type": "object", + "properties": { + "type": { + "const": "apiKey" + } + }, + "required": ["type"] }, - { - "$ref": "#/definitions/OAuth2SecurityScheme" + "then": { + "$ref": "#/definitions/HTTPSecurityScheme" }, - { - "$ref": "#/definitions/OpenIdConnectSecurityScheme" + "else": { + "if": { + "type": "object", + "properties": { + "type": { + "const": "oauth2" + } + }, + "required": ["type"] + }, + "then": { + "$ref": "#/definitions/OAuth2SecurityScheme" + }, + "else": { + "if": { + "type": "object", + "properties": { + "type": { + "const": "openIdConnect" + } + }, + "required": ["type"] + }, + "then": { + "$ref": "#/definitions/OpenIdConnectSecurityScheme" + }, + "else": { + "type": "object", + "properties": { + "type": { + "enum": ["apiKey", "http", "oauth2", "openIdConnect"] + } + }, + "required": ["type"], + "errorMessage": { + "required": "Security scheme must have a valid type", + "type": "Invalid security scheme" + } + } + } } - ] + } }, "APIKeySecurityScheme": { "type": "object", "required": ["type", "name", "in"], "properties": { "type": { - "type": "string", - "enum": ["apiKey"] + "const": "apiKey" }, "name": { "type": "string" }, "in": { - "type": "string", "enum": ["header", "query", "cookie"] }, "description": { @@ -1216,7 +1384,7 @@ }, "type": { "type": "string", - "enum": ["http"] + "const": "http" } }, "patternProperties": { @@ -1228,7 +1396,7 @@ "description": "Bearer", "properties": { "scheme": { - "enum": ["bearer"] + "const": "bearer" } } }, @@ -1240,7 +1408,7 @@ "properties": { "scheme": { "not": { - "enum": ["bearer"] + "const": "bearer" } } } @@ -1252,8 +1420,7 @@ "required": ["type", "flows"], "properties": { "type": { - "type": "string", - "enum": ["oauth2"] + "const": "oauth2" }, "flows": { "$ref": "#/definitions/OAuthFlows" @@ -1272,8 +1439,7 @@ "required": ["type", "openIdConnectUrl"], "properties": { "type": { - "type": "string", - "enum": ["openIdConnect"] + "const": "openIdConnect" }, "openIdConnectUrl": { "type": "string", @@ -1462,7 +1628,6 @@ } }, "style": { - "type": "string", "enum": ["form", "spaceDelimited", "pipeDelimited", "deepObject"] }, "explode": { diff --git a/packages/rulesets/src/oas/schemas/oas/v3.1/dialect.schema.json b/packages/rulesets/src/oas/schemas/oas/v3.1/dialect.schema.json new file mode 100644 index 000000000..3b2064572 --- /dev/null +++ b/packages/rulesets/src/oas/schemas/oas/v3.1/dialect.schema.json @@ -0,0 +1,25 @@ +{ + "$id": "https://spec.openapis.org/oas/3.1/dialect/base", + "$schema": "https://json-schema.org/draft/2020-12/schema", + + "title": "OpenAPI 3.1 Schema Object Dialect", + "description": "A JSON Schema dialect describing schemas found in OpenAPI documents", + + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/content": true, + "https://spec.openapis.org/oas/3.1/vocab/base": false + }, + + "$dynamicAnchor": "meta", + + "allOf": [ + { "$ref": "https://json-schema.org/draft/2020-12/schema" }, + { "$ref": "https://spec.openapis.org/oas/3.1/meta/base" } + ] +} diff --git a/packages/rulesets/src/oas/schemas/3.1.json b/packages/rulesets/src/oas/schemas/oas/v3.1/index.json similarity index 82% rename from packages/rulesets/src/oas/schemas/3.1.json rename to packages/rulesets/src/oas/schemas/oas/v3.1/index.json index eab8be0aa..51b7c14de 100644 --- a/packages/rulesets/src/oas/schemas/3.1.json +++ b/packages/rulesets/src/oas/schemas/oas/v3.1/index.json @@ -1,6 +1,7 @@ { "$id": "https://spec.openapis.org/oas/3.1/schema/2021-09-28", "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "The description of OpenAPI v3.1.x documents without schema validation, as defined by https://spec.openapis.org/oas/v3.1.0", "type": "object", "properties": { "openapi": { @@ -19,7 +20,12 @@ "type": "array", "items": { "$ref": "#/$defs/server" - } + }, + "default": [ + { + "url": "/" + } + ] }, "paths": { "$ref": "#/$defs/paths" @@ -50,15 +56,20 @@ } }, "required": ["openapi", "info"], + "errorMessage": { + "anyOf": "The document must have either \"paths\", \"webhooks\" or \"components\"" + }, "anyOf": [ { - "required": ["paths"], - "errorMessage": "The document must have either \"paths\", \"webhooks\" or \"components\"" + "errorMessage": "The document must have either \"paths\", \"webhooks\" or \"components\"", + "required": ["paths"] }, { + "errorMessage": "The document must have either \"paths\", \"webhooks\" or \"components\"", "required": ["components"] }, { + "errorMessage": "The document must have either \"paths\", \"webhooks\" or \"components\"", "required": ["webhooks"] } ], @@ -66,6 +77,7 @@ "unevaluatedProperties": false, "$defs": { "info": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#info-object", "type": "object", "properties": { "title": { @@ -78,7 +90,8 @@ "type": "string" }, "termsOfService": { - "type": "string" + "type": "string", + "format": "uri" }, "contact": { "$ref": "#/$defs/contact" @@ -95,22 +108,26 @@ "unevaluatedProperties": false }, "contact": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#contact-object", "type": "object", "properties": { "name": { "type": "string" }, "url": { - "type": "string" + "type": "string", + "format": "uri" }, "email": { - "type": "string" + "type": "string", + "format": "email" } }, "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "license": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#license-object", "type": "object", "properties": { "name": { @@ -125,23 +142,22 @@ } }, "required": ["name"], - "oneOf": [ - { - "required": ["identifier"] - }, - { - "required": ["url"] + "dependentSchemas": { + "identifier": { + "not": { + "required": ["url"] + } } - ], + }, "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "server": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#server-object", "type": "object", "properties": { "url": { - "type": "string", - "format": "uri-template" + "type": "string" }, "description": { "type": "string" @@ -158,6 +174,7 @@ "unevaluatedProperties": false }, "server-variable": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#server-variable-object", "type": "object", "properties": { "enum": { @@ -179,12 +196,13 @@ "unevaluatedProperties": false }, "components": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#components-object", "type": "object", "properties": { "schemas": { "type": "object", "additionalProperties": { - "$ref": "#/$defs/schema" + "$ref": "https://spec.openapis.org/oas/3.1/dialect/base" } }, "responses": { @@ -254,6 +272,7 @@ "unevaluatedProperties": false }, "paths": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#paths-object", "type": "object", "patternProperties": { "^/": { @@ -264,6 +283,7 @@ "unevaluatedProperties": false }, "path-item": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#path-item-object", "type": "object", "properties": { "summary": { @@ -283,10 +303,29 @@ "items": { "$ref": "#/$defs/parameter-or-reference" } - } - }, - "patternProperties": { - "^(get|put|post|delete|options|head|patch|trace)$": { + }, + "get": { + "$ref": "#/$defs/operation" + }, + "put": { + "$ref": "#/$defs/operation" + }, + "post": { + "$ref": "#/$defs/operation" + }, + "delete": { + "$ref": "#/$defs/operation" + }, + "options": { + "$ref": "#/$defs/operation" + }, + "head": { + "$ref": "#/$defs/operation" + }, + "patch": { + "$ref": "#/$defs/operation" + }, + "trace": { "$ref": "#/$defs/operation" } }, @@ -306,6 +345,7 @@ } }, "operation": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#operation-object", "type": "object", "properties": { "tags": { @@ -365,6 +405,7 @@ "unevaluatedProperties": false }, "external-documentation": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#external-documentation-object", "type": "object", "properties": { "description": { @@ -380,6 +421,7 @@ "unevaluatedProperties": false }, "parameter": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#parameter-object", "type": "object", "properties": { "name": { @@ -399,18 +441,16 @@ "default": false, "type": "boolean" }, - "allowEmptyValue": { - "default": false, - "type": "boolean" - }, "schema": { - "$ref": "#/$defs/schema" + "$ref": "https://spec.openapis.org/oas/3.1/dialect/base" }, "content": { - "$ref": "#/$defs/content" + "$ref": "#/$defs/content", + "minProperties": 1, + "maxProperties": 1 } }, - "required": ["in"], + "required": ["name", "in"], "oneOf": [ { "required": ["schema"] @@ -419,6 +459,22 @@ "required": ["content"] } ], + "if": { + "properties": { + "in": { + "const": "query" + } + }, + "required": ["in"] + }, + "then": { + "properties": { + "allowEmptyValue": { + "default": false, + "type": "boolean" + } + } + }, "dependentSchemas": { "schema": { "properties": { @@ -427,10 +483,6 @@ }, "explode": { "type": "boolean" - }, - "allowReserved": { - "default": false, - "type": "boolean" } }, "allOf": [ @@ -450,7 +502,7 @@ "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-cookie" }, { - "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-form" + "$ref": "#/$defs/styles-for-form" } ], "$defs": { @@ -465,9 +517,6 @@ }, "then": { "properties": { - "name": { - "pattern": "[^/#?]+$" - }, "style": { "default": "simple", "enum": ["matrix", "label", "simple"] @@ -511,6 +560,10 @@ "style": { "default": "form", "enum": ["form", "spaceDelimited", "pipeDelimited", "deepObject"] + }, + "allowReserved": { + "default": false, + "type": "boolean" } } } @@ -532,30 +585,6 @@ } } } - }, - "styles-for-form": { - "if": { - "properties": { - "style": { - "const": "form" - } - }, - "required": ["style"] - }, - "then": { - "properties": { - "explode": { - "default": true - } - } - }, - "else": { - "properties": { - "explode": { - "default": false - } - } - } } } } @@ -576,6 +605,7 @@ } }, "request-body": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#request-body-object", "type": "object", "properties": { "description": { @@ -606,6 +636,7 @@ } }, "content": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#fixed-fields-10", "type": "object", "additionalProperties": { "$ref": "#/$defs/media-type" @@ -615,10 +646,11 @@ } }, "media-type": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#media-type-object", "type": "object", "properties": { "schema": { - "$ref": "#/$defs/schema" + "$ref": "https://spec.openapis.org/oas/3.1/dialect/base" }, "encoding": { "type": "object", @@ -638,6 +670,7 @@ "unevaluatedProperties": false }, "encoding": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#encoding-object", "type": "object", "properties": { "contentType": { @@ -667,38 +700,13 @@ "$ref": "#/$defs/specification-extensions" }, { - "$ref": "#/$defs/encoding/$defs/explode-default" + "$ref": "#/$defs/styles-for-form" } ], - "unevaluatedProperties": false, - "$defs": { - "explode-default": { - "if": { - "properties": { - "style": { - "const": "form" - } - }, - "required": ["style"] - }, - "then": { - "properties": { - "explode": { - "default": true - } - } - }, - "else": { - "properties": { - "explode": { - "default": false - } - } - } - } - } + "unevaluatedProperties": false }, "responses": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#responses-object", "type": "object", "properties": { "default": { @@ -710,10 +718,21 @@ "$ref": "#/$defs/response-or-reference" } }, + "minProperties": 1, "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false + "unevaluatedProperties": false, + "if": { + "$comment": "either default, or at least one response code property must exist", + "patternProperties": { + "^[1-5](?:[0-9]{2}|XX)$": false + } + }, + "then": { + "required": ["default"] + } }, "response": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#response-object", "type": "object", "properties": { "description": { @@ -752,6 +771,7 @@ } }, "callbacks": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#callback-object", "type": "object", "$ref": "#/$defs/specification-extensions", "additionalProperties": { @@ -771,6 +791,7 @@ } }, "example": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#example-object", "type": "object", "properties": { "summary": { @@ -785,6 +806,9 @@ "format": "uri" } }, + "not": { + "required": ["value", "externalValue"] + }, "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, @@ -801,13 +825,16 @@ } }, "link": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#link-object", "type": "object", "properties": { "operationRef": { "type": "string", "format": "uri-reference" }, - "operationId": true, + "operationId": { + "type": "string" + }, "parameters": { "$ref": "#/$defs/map-of-strings" }, @@ -843,6 +870,7 @@ } }, "header": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#header-object", "type": "object", "properties": { "description": { @@ -857,10 +885,12 @@ "type": "boolean" }, "schema": { - "$ref": "#/$defs/schema" + "$ref": "https://spec.openapis.org/oas/3.1/dialect/base" }, "content": { - "$ref": "#/$defs/content" + "$ref": "#/$defs/content", + "minProperties": 1, + "maxProperties": 1 } }, "oneOf": [ @@ -902,6 +932,7 @@ } }, "tag": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#tag-object", "type": "object", "properties": { "name": { @@ -919,6 +950,7 @@ "unevaluatedProperties": false }, "reference": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#reference-object", "type": "object", "properties": { "$ref": { @@ -931,14 +963,15 @@ "description": { "type": "string" } - }, - "unevaluatedProperties": false + } }, "schema": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#schema-object", "$dynamicAnchor": "meta", "type": ["object", "boolean"] }, "security-scheme": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#security-scheme-object", "type": "object", "properties": { "type": { @@ -1105,10 +1138,12 @@ "type": "object", "properties": { "authorizationUrl": { - "type": "string" + "type": "string", + "format": "uri" }, "refreshUrl": { - "type": "string" + "type": "string", + "format": "uri" }, "scopes": { "$ref": "#/$defs/map-of-strings" @@ -1122,10 +1157,12 @@ "type": "object", "properties": { "tokenUrl": { - "type": "string" + "type": "string", + "format": "uri" }, "refreshUrl": { - "type": "string" + "type": "string", + "format": "uri" }, "scopes": { "$ref": "#/$defs/map-of-strings" @@ -1139,10 +1176,12 @@ "type": "object", "properties": { "tokenUrl": { - "type": "string" + "type": "string", + "format": "uri" }, "refreshUrl": { - "type": "string" + "type": "string", + "format": "uri" }, "scopes": { "$ref": "#/$defs/map-of-strings" @@ -1156,13 +1195,16 @@ "type": "object", "properties": { "authorizationUrl": { - "type": "string" + "type": "string", + "format": "uri" }, "tokenUrl": { - "type": "string" + "type": "string", + "format": "uri" }, "refreshUrl": { - "type": "string" + "type": "string", + "format": "uri" }, "scopes": { "$ref": "#/$defs/map-of-strings" @@ -1175,6 +1217,7 @@ } }, "security-requirement": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#security-requirement-object", "type": "object", "additionalProperties": { "type": "array", @@ -1184,6 +1227,7 @@ } }, "specification-extensions": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#specification-extensions", "patternProperties": { "^x-": true } @@ -1204,6 +1248,30 @@ "additionalProperties": { "type": "string" } + }, + "styles-for-form": { + "if": { + "properties": { + "style": { + "const": "form" + } + }, + "required": ["style"] + }, + "then": { + "properties": { + "explode": { + "default": true + } + } + }, + "else": { + "properties": { + "explode": { + "default": false + } + } + } } } } diff --git a/packages/rulesets/src/oas/schemas/oas/v3.1/meta.schema.json b/packages/rulesets/src/oas/schemas/oas/v3.1/meta.schema.json new file mode 100644 index 000000000..e8a20ef48 --- /dev/null +++ b/packages/rulesets/src/oas/schemas/oas/v3.1/meta.schema.json @@ -0,0 +1,87 @@ +{ + "$id": "https://spec.openapis.org/oas/3.1/meta/base", + "$schema": "https://json-schema.org/draft/2020-12/schema", + + "title": "OAS Base vocabulary", + "description": "A JSON Schema Vocabulary used in the OpenAPI Schema Dialect", + + "$vocabulary": { + "https://spec.openapis.org/oas/3.1/vocab/base": true + }, + + "$dynamicAnchor": "meta", + + "type": ["object", "boolean"], + "properties": { + "example": true, + "discriminator": { "$ref": "#/$defs/discriminator" }, + "externalDocs": { "$ref": "#/$defs/external-docs" }, + "xml": { "$ref": "#/$defs/xml" } + }, + + "$defs": { + "extensible": { + "patternProperties": { + "^x-": true + } + }, + + "discriminator": { + "$ref": "#/$defs/extensible", + "type": "object", + "properties": { + "propertyName": { + "type": "string" + }, + "mapping": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["propertyName"], + "unevaluatedProperties": false + }, + + "external-docs": { + "$ref": "#/$defs/extensible", + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri-reference" + }, + "description": { + "type": "string" + } + }, + "required": ["url"], + "unevaluatedProperties": false + }, + + "xml": { + "$ref": "#/$defs/extensible", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string", + "format": "uri" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean" + }, + "wrapped": { + "type": "boolean" + } + }, + "unevaluatedProperties": false + } + } +} diff --git a/test-harness/scenarios/oas3-schema.scenario b/test-harness/scenarios/oas3-schema.scenario index 552835f71..45ce3e10b 100644 --- a/test-harness/scenarios/oas3-schema.scenario +++ b/test-harness/scenarios/oas3-schema.scenario @@ -60,14 +60,15 @@ module.exports = { {bin} lint {document} --ruleset "{asset:ruleset}" ====stdout==== {document} - 6:10 error oas3-schema Property "foo" is not expected to be here. info.contact.foo - 12:17 error oas3-schema Property "type" is not expected to be here. paths./user.get.parameters[0].type - 23:18 error oas3-schema "type" property type must be string. paths./user.get.parameters[1].schema.type - 24:11 error oas3-schema "2" property must have required property "schema". paths./user.get.parameters[2] - 26:17 error oas3-schema Property "type" is not expected to be here. paths./user.get.parameters[2].type - 37:28 error oas3-schema "user_id" property type must be object. paths./user.get.responses[200].content.application/json.schema.properties.user_id - 41:28 error oas3-schema "properties" property type must be object. paths./user.get.responses[200].content.application/yaml.schema.properties - 43:23 error oas3-schema "description" property type must be string. paths./user.get.responses[400].description - 46:17 error oas3-schema "responses" property must not have fewer than 1 items. paths./address.get.responses + 6:10 error oas3-schema Property "foo" is not expected to be here. info.contact.foo + 12:17 error oas3-schema Property "type" is not expected to be here. paths./user.get.parameters[0].type + 20:15 error oas3-schema "in" property must be equal to one of the allowed values: "path", "query", "header", "cookie". Did you mean "path"?. paths./user.get.parameters[1].in + 23:18 error oas3-schema "type" property must be equal to one of the allowed values: "array", "boolean", "integer", "number", "object", "string". paths./user.get.parameters[1].schema.type + 24:11 error oas3-schema "schema" or "content" must be present. paths./user.get.parameters[2] + 26:17 error oas3-schema Property "type" is not expected to be here. paths./user.get.parameters[2].type + 37:28 error oas3-schema "user_id" property must be object. paths./user.get.responses[200].content.application/json.schema.properties.user_id + 41:28 error oas3-schema "properties" property must be object. paths./user.get.responses[200].content.application/yaml.schema.properties + 43:23 error oas3-schema "description" property must be string. paths./user.get.responses[400].description + 46:17 error oas3-schema "responses" property must not have fewer than 1 properties. paths./address.get.responses -✖ 9 problems (9 errors, 0 warnings, 0 infos, 0 hints) +✖ 10 problems (10 errors, 0 warnings, 0 infos, 0 hints) diff --git a/test-harness/scenarios/oas3.1/petstore.scenario b/test-harness/scenarios/oas3.1/petstore.scenario index d5c776053..a7bb6f96b 100644 --- a/test-harness/scenarios/oas3.1/petstore.scenario +++ b/test-harness/scenarios/oas3.1/petstore.scenario @@ -135,7 +135,8 @@ paths: description: OK summary: Create or replace your avatar. parameters: - - schema: + - name: avatar + schema: type: string in: header components: diff --git a/yarn.lock b/yarn.lock index 4782b56b6..f2fb6dfc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1755,14 +1755,14 @@ __metadata: languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.2": - version: 0.3.2 - resolution: "@jridgewell/gen-mapping@npm:0.3.2" +"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.2": + version: 0.3.3 + resolution: "@jridgewell/gen-mapping@npm:0.3.3" dependencies: "@jridgewell/set-array": ^1.0.1 "@jridgewell/sourcemap-codec": ^1.4.10 "@jridgewell/trace-mapping": ^0.3.9 - checksum: 1832707a1c476afebe4d0fbbd4b9434fdb51a4c3e009ab1e9938648e21b7a97049fa6009393bdf05cab7504108413441df26d8a3c12193996e65493a4efb6882 + checksum: 4a74944bd31f22354fc01c3da32e83c19e519e3bbadafa114f6da4522ea77dd0c2842607e923a591d60a76699d819a2fbb6f3552e277efdb9b58b081390b60ab languageName: node linkType: hard @@ -1780,6 +1780,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/source-map@npm:^0.3.3": + version: 0.3.5 + resolution: "@jridgewell/source-map@npm:0.3.5" + dependencies: + "@jridgewell/gen-mapping": ^0.3.0 + "@jridgewell/trace-mapping": ^0.3.9 + checksum: 1ad4dec0bdafbade57920a50acec6634f88a0eb735851e0dda906fa9894e7f0549c492678aad1a10f8e144bfe87f238307bf2a914a1bc85b7781d345417e9f6f + languageName: node + linkType: hard + "@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14": version: 1.4.15 resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" @@ -2838,11 +2848,14 @@ __metadata: "@stoplight/spectral-runtime": ^1.1.1 "@stoplight/types": ^13.6.0 "@types/json-schema": ^7.0.7 - ajv: ^8.8.2 + ajv: ^8.12.0 ajv-formats: ~2.1.0 + gzip-size: ^6.0.0 immer: ^9.0.6 json-schema-traverse: ^1.0.0 + leven: 3.1.0 lodash: ~4.17.21 + terser: ^5.26.0 tslib: ^2.3.0 languageName: unknown linkType: soft @@ -3627,12 +3640,12 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.1.0, acorn@npm:^8.4.1, acorn@npm:^8.8.0": - version: 8.8.0 - resolution: "acorn@npm:8.8.0" +"acorn@npm:^8.1.0, acorn@npm:^8.4.1, acorn@npm:^8.8.0, acorn@npm:^8.8.2": + version: 8.11.3 + resolution: "acorn@npm:8.11.3" bin: acorn: bin/acorn - checksum: 7270ca82b242eafe5687a11fea6e088c960af712683756abf0791b68855ea9cace3057bd5e998ffcef50c944810c1e0ca1da526d02b32110e13c722aa959afdc + checksum: 76d8e7d559512566b43ab4aadc374f11f563f0a9e21626dd59cb2888444e9445923ae9f3699972767f18af61df89cd89f5eaaf772d1327b055b45cb829b4a88c languageName: node linkType: hard @@ -3722,15 +3735,15 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.0.0, 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" +"ajv@npm:^8.0.0, ajv@npm:^8.12.0, ajv@npm:^8.6.0, ajv@npm:^8.6.3": + version: 8.12.0 + resolution: "ajv@npm:8.12.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: 4dc13714e316e67537c8b31bc063f99a1d9d9a497eb4bbd55191ac0dcd5e4985bbb71570352ad6f1e76684fb6d790928f96ba3b2d4fd6e10024be9612fe3f001 languageName: node linkType: hard @@ -4854,6 +4867,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^2.20.0": + version: 2.20.3 + resolution: "commander@npm:2.20.3" + checksum: ab8c07884e42c3a8dbc5dd9592c606176c7eb5c1ca5ff274bcf907039b2c41de3626f684ea75ccf4d361ba004bbaff1f577d5384c155f3871e456bdf27becf9e + languageName: node + linkType: hard + "commander@npm:^8.2.0": version: 8.2.0 resolution: "commander@npm:8.2.0" @@ -5602,6 +5622,13 @@ __metadata: languageName: node linkType: hard +"duplexer@npm:^0.1.2": + version: 0.1.2 + resolution: "duplexer@npm:0.1.2" + checksum: 62ba61a830c56801db28ff6305c7d289b6dc9f859054e8c982abd8ee0b0a14d2e9a8e7d086ffee12e868d43e2bbe8a964be55ddbd8c8957714c87373c7a4f9b0 + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -6919,6 +6946,15 @@ __metadata: languageName: node linkType: hard +"gzip-size@npm:^6.0.0": + version: 6.0.0 + resolution: "gzip-size@npm:6.0.0" + dependencies: + duplexer: ^0.1.2 + checksum: 2df97f359696ad154fc171dcb55bc883fe6e833bca7a65e457b9358f3cb6312405ed70a8da24a77c1baac0639906cd52358dc0ce2ec1a937eaa631b934c94194 + languageName: node + linkType: hard + "handlebars@npm:^4.7.7": version: 4.7.7 resolution: "handlebars@npm:4.7.7" @@ -8701,7 +8737,7 @@ __metadata: languageName: node linkType: hard -"leven@npm:^3.1.0": +"leven@npm:3.1.0, leven@npm:^3.1.0": version: 3.1.0 resolution: "leven@npm:3.1.0" checksum: 638401d534585261b6003db9d99afd244dfe82d75ddb6db5c0df412842d5ab30b2ef18de471aaec70fe69a46f17b4ae3c7f01d8a4e6580ef7adb9f4273ad1e55 @@ -11919,13 +11955,13 @@ __metadata: languageName: node linkType: hard -"source-map-support@npm:^0.5.17": - version: 0.5.19 - resolution: "source-map-support@npm:0.5.19" +"source-map-support@npm:^0.5.17, source-map-support@npm:~0.5.20": + version: 0.5.21 + resolution: "source-map-support@npm:0.5.21" dependencies: buffer-from: ^1.0.0 source-map: ^0.6.0 - checksum: c72802fdba9cb62b92baef18cc14cc4047608b77f0353e6c36dd993444149a466a2845332c5540d4a6630957254f0f68f4ef5a0120c33d2e83974c51a05afbac + checksum: 43e98d700d79af1d36f859bdb7318e601dfc918c7ba2e98456118ebc4c4872b327773e5a1df09b0524e9e5063bb18f0934538eace60cca2710d1fa687645d137 languageName: node linkType: hard @@ -12398,6 +12434,20 @@ __metadata: languageName: node linkType: hard +"terser@npm:^5.26.0": + version: 5.26.0 + resolution: "terser@npm:5.26.0" + dependencies: + "@jridgewell/source-map": ^0.3.3 + acorn: ^8.8.2 + commander: ^2.20.0 + source-map-support: ~0.5.20 + bin: + terser: bin/terser + checksum: 02a9bb896f04df828025af8f0eced36c315d25d310b6c2418e7dad2bed19ddeb34a9cea9b34e7c24789830fa51e1b6a9be26679980987a9c817a7e6d9cd4154b + languageName: node + linkType: hard + "test-exclude@npm:^6.0.0": version: 6.0.0 resolution: "test-exclude@npm:6.0.0" From d51690b5b4df94bc5521c76a4456901333d04543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Fri, 12 Jan 2024 14:31:09 +0100 Subject: [PATCH 2/6] build(rulesets): try sth --- packages/rulesets/package.json | 6 +- packages/rulesets/scripts/bundle.ts | 37 ++ packages/rulesets/scripts/compile-schemas.ts | 30 +- yarn.lock | 443 ++++++++++++++++++- 4 files changed, 466 insertions(+), 50 deletions(-) create mode 100644 packages/rulesets/scripts/bundle.ts diff --git a/packages/rulesets/package.json b/packages/rulesets/package.json index 8f8483227..fca3d4a96 100644 --- a/packages/rulesets/package.json +++ b/packages/rulesets/package.json @@ -50,15 +50,17 @@ "tslib": "^2.3.0" }, "devDependencies": { + "@rollup/plugin-sucrase": "^5.0.2", + "@rollup/plugin-terser": "^0.4.4", "@stoplight/path": "^1.3.2", "@stoplight/spectral-parsers": "*", "@stoplight/spectral-ref-resolver": "*", "gzip-size": "^6.0.0", - "immer": "^9.0.6", - "terser": "^5.26.0" + "immer": "^9.0.6" }, "scripts": { "compile-schemas": "ts-node -T ./scripts/compile-schemas.ts", + "postbuild": "ts-node -T ./scripts/bundle.ts", "prelint": "yarn compile-schemas --quiet", "pretest": "yarn compile-schemas --quiet", "prebuild": "yarn compile-schemas --quiet" diff --git a/packages/rulesets/scripts/bundle.ts b/packages/rulesets/scripts/bundle.ts new file mode 100644 index 000000000..5c7cc886b --- /dev/null +++ b/packages/rulesets/scripts/bundle.ts @@ -0,0 +1,37 @@ +import { rollup } from 'rollup'; +import terser from '@rollup/plugin-terser'; +import * as path from 'path'; +import sucrase from '@rollup/plugin-sucrase'; + +const cwd = path.join(__dirname, '..'); + +rollup({ + input: path.join(cwd, 'src/oas/functions/_oasDocumentSchema.ts'), + plugins: [ + sucrase({ + transforms: ['typescript'], + }), + terser({ + ecma: 2020, + module: true, + compress: { + passes: 2, + }, + mangle: { + // properties: true, + // reserved: ['message', 'path'], + }, + }), + ], + treeshake: true, + watch: false, + perf: false, + external: ['@stoplight/spectral-core', '@stoplight/json', 'leven'], +}).then(bundle => + bundle.write({ + format: 'commonjs', + exports: 'default', + sourcemap: true, + file: path.join(cwd, 'dist/oas/functions/_oasDocumentSchema.js'), + }), +); diff --git a/packages/rulesets/scripts/compile-schemas.ts b/packages/rulesets/scripts/compile-schemas.ts index e73aef3a9..4b354ff44 100644 --- a/packages/rulesets/scripts/compile-schemas.ts +++ b/packages/rulesets/scripts/compile-schemas.ts @@ -7,8 +7,6 @@ import standaloneCode from 'ajv/dist/standalone/index.js'; import ajvErrors from 'ajv-errors'; import ajvFormats from 'ajv-formats'; import chalk from 'chalk'; -import { minify } from 'terser'; -import { sync } from 'gzip-size'; const cwd = path.join(__dirname, '../src'); @@ -56,33 +54,9 @@ Promise.all(schemas) oas3_1: 'https://spec.openapis.org/oas/3.1/schema/2021-09-28', }); - const minified = ( - await minify(code, { - compress: { - passes: 2, - ecma: 2020, - }, - ecma: 2020, - module: true, - mangle: { - toplevel: true, - module: true, - }, - format: { - comments: false, - }, - }) - ).code!; + log('writing %s size is %dKB', path.join(target, '..', basename), Math.round((code.length / 1024) * 100) / 100); - log( - 'writing %s size is %dKB (original), %dKB (minified) %dKB (minified + gzipped)', - path.join(target, '..', basename), - Math.round((code.length / 1024) * 100) / 100, - Math.round((minified.length / 1024) * 100) / 100, - Math.round((sync(minified) / 1024) * 100) / 100, - ); - - await fs.promises.writeFile(path.join(target, '..', basename), ['// @ts-nocheck', minified].join('\n')); + await fs.promises.writeFile(path.join(target, '..', basename), ['// @ts-nocheck', code].join('\n')); }) .then(() => { log(chalk.green('Validators generated.')); diff --git a/yarn.lock b/yarn.lock index f2fb6dfc8..fc49ab2fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1466,6 +1466,20 @@ __metadata: languageName: node linkType: hard +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: ^5.1.2 + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: ^7.0.1 + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: ^8.1.0 + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 4a473b9b32a7d4d3cfb7a614226e555091ff0c5a29a1734c28c72a182c2f6699b26fc6b5c2131dfd841e86b185aea714c72201d7c98c2fba5f17709333a67aeb + languageName: node + linkType: hard + "@isaacs/string-locale-compare@npm:^1.1.0": version: 1.1.0 resolution: "@isaacs/string-locale-compare@npm:1.1.0" @@ -2340,6 +2354,13 @@ __metadata: languageName: node linkType: hard +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f + languageName: node + linkType: hard + "@pnpm/config.env-replace@npm:^1.1.0": version: 1.1.0 resolution: "@pnpm/config.env-replace@npm:1.1.0" @@ -2384,6 +2405,37 @@ __metadata: languageName: node linkType: hard +"@rollup/plugin-sucrase@npm:^5.0.2": + version: 5.0.2 + resolution: "@rollup/plugin-sucrase@npm:5.0.2" + dependencies: + "@rollup/pluginutils": ^5.0.1 + sucrase: ^3.27.0 + peerDependencies: + rollup: ^2.53.1||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 3780cc4a6e3cc492a25e0eb0efb2f99e353f1c05497c97d52c901f07ef262ea114de312cba3a6b6d86cca49e28817d85e4bbc9b060c32f94d7364d8c8f2198a7 + languageName: node + linkType: hard + +"@rollup/plugin-terser@npm:^0.4.4": + version: 0.4.4 + resolution: "@rollup/plugin-terser@npm:0.4.4" + dependencies: + serialize-javascript: ^6.0.1 + smob: ^1.0.0 + terser: ^5.17.4 + peerDependencies: + rollup: ^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 5472f659fbb7034488df91eb01ecd2ddf6d2cf203d049aa486139225ad5566254c6ec24aad1f5d1167e35f480212ede5160df9cc80e149a28874f78ed6a7fd9a + languageName: node + linkType: hard + "@rollup/pluginutils@npm:^3.1.0": version: 3.1.0 resolution: "@rollup/pluginutils@npm:3.1.0" @@ -2397,6 +2449,22 @@ __metadata: languageName: node linkType: hard +"@rollup/pluginutils@npm:^5.0.1": + version: 5.1.0 + resolution: "@rollup/pluginutils@npm:5.1.0" + dependencies: + "@types/estree": ^1.0.0 + estree-walker: ^2.0.2 + picomatch: ^2.3.1 + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 3cc5a6d91452a6eabbfd1ae79b4dd1f1e809d2eecda6e175deb784e75b0911f47e9ecce73f8dd315d6a8b3f362582c91d3c0f66908b6ced69345b3cbe28f8ce8 + languageName: node + linkType: hard + "@semantic-release/changelog@npm:^6.0.3": version: 6.0.3 resolution: "@semantic-release/changelog@npm:6.0.3" @@ -2837,6 +2905,8 @@ __metadata: resolution: "@stoplight/spectral-rulesets@workspace:packages/rulesets" dependencies: "@asyncapi/specs": ^4.1.0 + "@rollup/plugin-sucrase": ^5.0.2 + "@rollup/plugin-terser": ^0.4.4 "@stoplight/better-ajv-errors": 1.0.3 "@stoplight/json": ^3.17.0 "@stoplight/path": ^1.3.2 @@ -2855,6 +2925,8 @@ __metadata: json-schema-traverse: ^1.0.0 leven: 3.1.0 lodash: ~4.17.21 + rollup-plugin-bundle-size: ^1.0.3 + rollup-plugin-output-size: ^1.3.0 terser: ^5.26.0 tslib: ^2.3.0 languageName: unknown @@ -3182,6 +3254,13 @@ __metadata: languageName: node linkType: hard +"@types/estree@npm:^1.0.0": + version: 1.0.5 + resolution: "@types/estree@npm:1.0.5" + checksum: dd8b5bed28e6213b7acd0fb665a84e693554d850b0df423ac8076cc3ad5823a6bc26b0251d080bdc545af83179ede51dd3f6fa78cad2c46ed1f29624ddf3e41a + languageName: node + linkType: hard + "@types/file-entry-cache@npm:^5.0.2": version: 5.0.2 resolution: "@types/file-entry-cache@npm:5.0.2" @@ -3786,6 +3865,20 @@ __metadata: languageName: node linkType: hard +"ansi-regex@npm:^6.0.1": + version: 6.0.1 + resolution: "ansi-regex@npm:6.0.1" + checksum: 1ff8b7667cded1de4fa2c9ae283e979fc87036864317da86a2e546725f96406746411d0d85e87a2d12fa5abd715d90006de7fa4fa0477c92321ad3b4c7d4e169 + languageName: node + linkType: hard + +"ansi-styles@npm:^2.2.1": + version: 2.2.1 + resolution: "ansi-styles@npm:2.2.1" + checksum: ebc0e00381f2a29000d1dac8466a640ce11943cef3bda3cd0020dc042e31e1058ab59bf6169cd794a54c3a7338a61ebc404b7c91e004092dd20e028c432c9c2c + languageName: node + linkType: hard + "ansi-styles@npm:^3.2.1": version: 3.2.1 resolution: "ansi-styles@npm:3.2.1" @@ -3811,6 +3904,13 @@ __metadata: languageName: node linkType: hard +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: ef940f2f0ced1a6347398da88a91da7930c33ecac3c77b72c5905f8b8fe402c52e6fde304ff5347f616e27a742da3f1dc76de98f6866c69251ad0b07a66776d9 + languageName: node + linkType: hard + "ansicolors@npm:~0.3.2": version: 0.3.2 resolution: "ansicolors@npm:0.3.2" @@ -4568,6 +4668,19 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^1.0.0, chalk@npm:^1.1.3": + version: 1.1.3 + resolution: "chalk@npm:1.1.3" + dependencies: + ansi-styles: ^2.2.1 + escape-string-regexp: ^1.0.2 + has-ansi: ^2.0.0 + strip-ansi: ^3.0.0 + supports-color: ^2.0.0 + checksum: 9d2ea6b98fc2b7878829eec223abcf404622db6c48396a9b9257f6d0ead2acf18231ae368d6a664a83f272b0679158da12e97b5229f794939e555cc574478acd + languageName: node + linkType: hard + "chalk@npm:^2.3.2, chalk@npm:^2.4.1, chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -4836,6 +4949,13 @@ __metadata: languageName: node linkType: hard +"colorette@npm:^2.0.20": + version: 2.0.20 + resolution: "colorette@npm:2.0.20" + checksum: 0c016fea2b91b733eb9f4bcdb580018f52c0bc0979443dad930e5037a968237ac53d9beb98e218d2e9235834f8eebce7f8e080422d6194e957454255bde71d3d + languageName: node + linkType: hard + "columnify@npm:^1.6.0": version: 1.6.0 resolution: "columnify@npm:1.6.0" @@ -4874,6 +4994,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^4.0.0": + version: 4.1.1 + resolution: "commander@npm:4.1.1" + checksum: d7b9913ff92cae20cb577a4ac6fcc121bd6223319e54a40f51a14740a681ad5c574fd29a57da478a5f234a6fa6c52cbf0b7c641353e03c648b1ae85ba670b977 + languageName: node + linkType: hard + "commander@npm:^8.2.0": version: 8.2.0 resolution: "commander@npm:8.2.0" @@ -5622,13 +5749,20 @@ __metadata: languageName: node linkType: hard -"duplexer@npm:^0.1.2": +"duplexer@npm:^0.1.1, duplexer@npm:^0.1.2": version: 0.1.2 resolution: "duplexer@npm:0.1.2" checksum: 62ba61a830c56801db28ff6305c7d289b6dc9f859054e8c982abd8ee0b0a14d2e9a8e7d086ffee12e868d43e2bbe8a964be55ddbd8c8957714c87373c7a4f9b0 languageName: node linkType: hard +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 7d00d7cd8e49b9afa762a813faac332dee781932d6f2c848dc348939c4253f1d4564341b7af1d041853bc3f32c2ef141b58e0a4d9862c17a7f08f68df1e0f1ed + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -5672,6 +5806,13 @@ __metadata: languageName: node linkType: hard +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: 8487182da74aabd810ac6d6f1994111dfc0e331b01271ae01ec1eb0ad7b5ecc2bbbbd2f053c05cb55a1ac30449527d819bbfbf0e3de1023db308cbcb47f86601 + languageName: node + linkType: hard + "encodeurl@npm:~1.0.2": version: 1.0.2 resolution: "encodeurl@npm:1.0.2" @@ -5904,7 +6045,7 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:^1.0.5": +"escape-string-regexp@npm:^1.0.2, escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" checksum: 6092fda75c63b110c706b6a9bfde8a612ad595b628f0bd2147eea1d3406723020810e591effc7db1da91d80a71a737a313567c5abb3813e8d9c71f4aa595b410 @@ -6148,7 +6289,7 @@ __metadata: languageName: node linkType: hard -"estree-walker@npm:^2.0.1": +"estree-walker@npm:^2.0.1, estree-walker@npm:^2.0.2": version: 2.0.2 resolution: "estree-walker@npm:2.0.2" checksum: 6151e6f9828abe2259e57f5fd3761335bb0d2ebd76dc1a01048ccee22fabcfef3c0859300f6d83ff0d1927849368775ec5a6d265dde2f6de5a1be1721cd94efc @@ -6389,6 +6530,16 @@ __metadata: languageName: node linkType: hard +"figures@npm:^1.0.1": + version: 1.7.0 + resolution: "figures@npm:1.7.0" + dependencies: + escape-string-regexp: ^1.0.5 + object-assign: ^4.1.0 + checksum: d77206deba991a7977f864b8c8edf9b8b43b441be005482db04b0526e36263adbdb22c1c6d2df15a1ad78d12029bd1aa41ccebcb5d425e1f2cf629c6daaa8e10 + languageName: node + linkType: hard + "figures@npm:^2.0.0": version: 2.0.0 resolution: "figures@npm:2.0.0" @@ -6514,6 +6665,16 @@ __metadata: languageName: node linkType: hard +"foreground-child@npm:^3.1.0": + version: 3.1.1 + resolution: "foreground-child@npm:3.1.1" + dependencies: + cross-spawn: ^7.0.0 + signal-exit: ^4.0.1 + checksum: 139d270bc82dc9e6f8bc045fe2aae4001dc2472157044fdfad376d0a3457f77857fa883c1c8b21b491c6caade9a926a4bed3d3d2e8d3c9202b151a4cbbd0bcd5 + languageName: node + linkType: hard + "form-data@npm:^3.0.0": version: 3.0.0 resolution: "form-data@npm:3.0.0" @@ -6836,6 +6997,21 @@ __metadata: languageName: node linkType: hard +"glob@npm:^10.3.10": + version: 10.3.10 + resolution: "glob@npm:10.3.10" + dependencies: + foreground-child: ^3.1.0 + jackspeak: ^2.3.5 + minimatch: ^9.0.1 + minipass: ^5.0.0 || ^6.0.2 || ^7.0.0 + path-scurry: ^1.10.1 + bin: + glob: dist/esm/bin.mjs + checksum: 4f2fe2511e157b5a3f525a54092169a5f92405f24d2aed3142f4411df328baca13059f4182f1db1bf933e2c69c0bd89e57ae87edd8950cba8c7ccbe84f721cf3 + languageName: node + linkType: hard + "glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -6946,6 +7122,15 @@ __metadata: languageName: node linkType: hard +"gzip-size@npm:^3.0.0": + version: 3.0.0 + resolution: "gzip-size@npm:3.0.0" + dependencies: + duplexer: ^0.1.1 + checksum: 683095068fc28e5dfa7dd77114ba95583d5acfd99e8028a993602e620eb9d48bf7910c14a3117caa9d665e3e1271b4027396f714be30f2b619dc638c76e5a6e8 + languageName: node + linkType: hard + "gzip-size@npm:^6.0.0": version: 6.0.0 resolution: "gzip-size@npm:6.0.0" @@ -6955,6 +7140,15 @@ __metadata: languageName: node linkType: hard +"gzip-size@npm:^7.0.0": + version: 7.0.0 + resolution: "gzip-size@npm:7.0.0" + dependencies: + duplexer: ^0.1.2 + checksum: 52d0bf586307082428b99f7b04d56d756d640e1f84d4a56debf9fb8c972d9db679143b067dd4024ebef42e9f6787e9dc8b9dcad344372b9dc87e55d942276f49 + languageName: node + linkType: hard + "handlebars@npm:^4.7.7": version: 4.7.7 resolution: "handlebars@npm:4.7.7" @@ -6980,6 +7174,15 @@ __metadata: languageName: node linkType: hard +"has-ansi@npm:^2.0.0": + version: 2.0.0 + resolution: "has-ansi@npm:2.0.0" + dependencies: + ansi-regex: ^2.0.0 + checksum: 1b51daa0214440db171ff359d0a2d17bc20061164c57e76234f614c91dbd2a79ddd68dfc8ee73629366f7be45a6df5f2ea9de83f52e1ca24433f2cc78c35d8ec + languageName: node + linkType: hard + "has-bigints@npm:^1.0.1": version: 1.0.1 resolution: "has-bigints@npm:1.0.1" @@ -7849,6 +8052,19 @@ __metadata: languageName: node linkType: hard +"jackspeak@npm:^2.3.5": + version: 2.3.6 + resolution: "jackspeak@npm:2.3.6" + dependencies: + "@isaacs/cliui": ^8.0.2 + "@pkgjs/parseargs": ^0.11.0 + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 57d43ad11eadc98cdfe7496612f6bbb5255ea69fe51ea431162db302c2a11011642f50cfad57288bd0aea78384a0612b16e131944ad8ecd09d619041c8531b54 + languageName: node + linkType: hard + "jasmine-core@npm:^4.1.0": version: 4.2.0 resolution: "jasmine-core@npm:4.2.0" @@ -9123,6 +9339,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^9.1.1 || ^10.0.0": + version: 10.1.0 + resolution: "lru-cache@npm:10.1.0" + checksum: 58056d33e2500fbedce92f8c542e7c11b50d7d086578f14b7074d8c241422004af0718e08a6eaae8705cee09c77e39a61c1c79e9370ba689b7010c152e6a76ab + languageName: node + linkType: hard + "lru-queue@npm:^0.1.0": version: 0.1.0 resolution: "lru-queue@npm:0.1.0" @@ -9253,6 +9476,18 @@ __metadata: languageName: node linkType: hard +"maxmin@npm:^2.1.0": + version: 2.1.0 + resolution: "maxmin@npm:2.1.0" + dependencies: + chalk: ^1.0.0 + figures: ^1.0.1 + gzip-size: ^3.0.0 + pretty-bytes: ^3.0.0 + checksum: 97e2377454c4b436df8cfe46cff95e8e6166a69b5256a6513d4afc3468eeee3d26eaaac153d26c7e7cef1f775c28c7d58b4399929d5472801b666a99581d0fdb + languageName: node + linkType: hard + "md5.js@npm:^1.3.4": version: 1.3.5 resolution: "md5.js@npm:1.3.5" @@ -9438,6 +9673,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^9.0.1": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: ^2.0.1 + checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 + languageName: node + linkType: hard + "minimist-options@npm:4.1.0": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" @@ -9548,6 +9792,13 @@ __metadata: languageName: node linkType: hard +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0": + version: 7.0.4 + resolution: "minipass@npm:7.0.4" + checksum: 87585e258b9488caf2e7acea242fd7856bbe9a2c84a7807643513a338d66f368c7d518200ad7b70a508664d408aa000517647b2930c259a8b1f9f0984f344a21 + languageName: node + linkType: hard + "minizlib@npm:^2.0.0, minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": version: 2.1.2 resolution: "minizlib@npm:2.1.2" @@ -10652,6 +10903,16 @@ __metadata: languageName: node linkType: hard +"path-scurry@npm:^1.10.1": + version: 1.10.1 + resolution: "path-scurry@npm:1.10.1" + dependencies: + lru-cache: ^9.1.1 || ^10.0.0 + minipass: ^5.0.0 || ^6.0.2 || ^7.0.0 + checksum: e2557cff3a8fb8bc07afdd6ab163a92587884f9969b05bbbaf6fe7379348bfb09af9ed292af12ed32398b15fb443e81692047b786d1eeb6d898a51eb17ed7d90 + languageName: node + linkType: hard + "path-to-regexp@npm:^2.2.1": version: 2.4.0 resolution: "path-to-regexp@npm:2.4.0" @@ -10693,6 +10954,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^2.3.1": + version: 2.3.1 + resolution: "picomatch@npm:2.3.1" + checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf + languageName: node + linkType: hard + "pify@npm:^3.0.0": version: 3.0.0 resolution: "pify@npm:3.0.0" @@ -10707,6 +10975,13 @@ __metadata: languageName: node linkType: hard +"pirates@npm:^4.0.1": + version: 4.0.6 + resolution: "pirates@npm:4.0.6" + checksum: 46a65fefaf19c6f57460388a5af9ab81e3d7fd0e7bc44ca59d753cb5c4d0df97c6c6e583674869762101836d68675f027d60f841c105d72734df9dfca97cbcc6 + languageName: node + linkType: hard + "pirates@npm:^4.0.4": version: 4.0.5 resolution: "pirates@npm:4.0.5" @@ -10806,6 +11081,22 @@ __metadata: languageName: node linkType: hard +"pretty-bytes@npm:^3.0.0": + version: 3.0.1 + resolution: "pretty-bytes@npm:3.0.1" + dependencies: + number-is-nan: ^1.0.0 + checksum: 0709a19bb30c0a35d84f2afdfbeaef3e68703c28346e85413493edd687f7509d1ec987cda2fe54554b9481426ba775f4cd6108c16633353768cfad4d417baacd + languageName: node + linkType: hard + +"pretty-bytes@npm:^6.1.1": + version: 6.1.1 + resolution: "pretty-bytes@npm:6.1.1" + checksum: 43d29d909d2d88072da2c3d72f8fd0f2d2523c516bfa640aff6e31f596ea1004b6601f4cabc50d14b2cf10e82635ebe5b7d9378f3d5bae1c0067131829421b8a + languageName: node + linkType: hard + "pretty-format@npm:^27.5.1": version: 27.5.1 resolution: "pretty-format@npm:27.5.1" @@ -11024,7 +11315,7 @@ __metadata: languageName: node linkType: hard -"randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5": +"randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5, randombytes@npm:^2.1.0": version: 2.1.0 resolution: "randombytes@npm:2.1.0" dependencies: @@ -11472,6 +11763,32 @@ __metadata: languageName: node linkType: hard +"rollup-plugin-bundle-size@npm:^1.0.3": + version: 1.0.3 + resolution: "rollup-plugin-bundle-size@npm:1.0.3" + dependencies: + chalk: ^1.1.3 + maxmin: ^2.1.0 + checksum: 21165474bbac68484c98e4a6346888511dca327da3d9b9d7ab15cb003c67a052443d8a599fb5647b7a312104d2740f246ba9b692754dda92be2a20d5f7fc4fd6 + languageName: node + linkType: hard + +"rollup-plugin-output-size@npm:^1.3.0": + version: 1.3.0 + resolution: "rollup-plugin-output-size@npm:1.3.0" + dependencies: + colorette: ^2.0.20 + gzip-size: ^7.0.0 + pretty-bytes: ^6.1.1 + peerDependencies: + rollup: ^2.0.0 || ^3.0.0 || ^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 47bda6876f9d12e5eccccc61c401ee624b511a3a8ffe2b75b597105603c77af653e4bb714918bcd7763a21fa68fb27e2caca2afe7cbece2e53fb564f7a895f71 + languageName: node + linkType: hard + "rollup@npm:~2.79.0": version: 2.79.0 resolution: "rollup@npm:2.79.0" @@ -11701,6 +12018,15 @@ __metadata: languageName: node linkType: hard +"serialize-javascript@npm:^6.0.1": + version: 6.0.2 + resolution: "serialize-javascript@npm:6.0.2" + dependencies: + randombytes: ^2.1.0 + checksum: c4839c6206c1d143c0f80763997a361310305751171dd95e4b57efee69b8f6edd8960a0b7fbfc45042aadff98b206d55428aee0dc276efe54f100899c7fa8ab7 + languageName: node + linkType: hard + "set-blocking@npm:^2.0.0, set-blocking@npm:~2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -11793,6 +12119,13 @@ __metadata: languageName: node linkType: hard +"signal-exit@npm:^4.0.1": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 64c757b498cb8629ffa5f75485340594d2f8189e9b08700e69199069c8e3070fb3e255f7ab873c05dc0b3cec412aea7402e10a5990cb6a050bd33ba062a6c549 + languageName: node + linkType: hard + "signale@npm:^1.2.1, signale@npm:^1.4.0": version: 1.4.0 resolution: "signale@npm:1.4.0" @@ -11881,6 +12214,13 @@ __metadata: languageName: node linkType: hard +"smob@npm:^1.0.0": + version: 1.4.1 + resolution: "smob@npm:1.4.1" + checksum: 3bd9e6bcc440356b0e06165f04f0ea170ebc1d57713e4a1d64c57227cb423d8346d3e0894fd7ce28bf75958f73a62f91ba13574a9a0fb4cbc271fa9ef5d75f4e + languageName: node + linkType: hard + "socket.io-adapter@npm:~2.4.0": version: 2.4.0 resolution: "socket.io-adapter@npm:2.4.0" @@ -12188,6 +12528,17 @@ __metadata: languageName: node linkType: hard +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" + dependencies: + emoji-regex: ^8.0.0 + is-fullwidth-code-point: ^3.0.0 + strip-ansi: ^6.0.1 + checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb + languageName: node + linkType: hard + "string-width@npm:^1.0.1": version: 1.0.2 resolution: "string-width@npm:1.0.2" @@ -12199,14 +12550,14 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": - version: 4.2.3 - resolution: "string-width@npm:4.2.3" +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" dependencies: - emoji-regex: ^8.0.0 - is-fullwidth-code-point: ^3.0.0 - strip-ansi: ^6.0.1 - checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb + eastasianwidth: ^0.2.0 + emoji-regex: ^9.2.2 + strip-ansi: ^7.0.1 + checksum: 7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193 languageName: node linkType: hard @@ -12259,7 +12610,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:6.0, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:6.0, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" dependencies: @@ -12277,6 +12628,15 @@ __metadata: languageName: node linkType: hard +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: ^6.0.1 + checksum: 859c73fcf27869c22a4e4d8c6acfe690064659e84bef9458aa6d13719d09ca88dcfd40cbf31fd0be63518ea1a643fe070b4827d353e09533a5b0b9fd4553d64d + languageName: node + linkType: hard + "strip-bom@npm:^3.0.0": version: 3.0.0 resolution: "strip-bom@npm:3.0.0" @@ -12321,6 +12681,24 @@ __metadata: languageName: node linkType: hard +"sucrase@npm:^3.27.0": + version: 3.35.0 + resolution: "sucrase@npm:3.35.0" + dependencies: + "@jridgewell/gen-mapping": ^0.3.2 + commander: ^4.0.0 + glob: ^10.3.10 + lines-and-columns: ^1.1.6 + mz: ^2.7.0 + pirates: ^4.0.1 + ts-interface-checker: ^0.1.9 + bin: + sucrase: bin/sucrase + sucrase-node: bin/sucrase-node + checksum: 9fc5792a9ab8a14dcf9c47dcb704431d35c1cdff1d17d55d382a31c2e8e3063870ad32ce120a80915498486246d612e30cda44f1624d9d9a10423e1a43487ad1 + languageName: node + linkType: hard + "supports-color@npm:8.1.1, supports-color@npm:^8.0.0": version: 8.1.1 resolution: "supports-color@npm:8.1.1" @@ -12330,6 +12708,13 @@ __metadata: languageName: node linkType: hard +"supports-color@npm:^2.0.0": + version: 2.0.0 + resolution: "supports-color@npm:2.0.0" + checksum: 602538c5812b9006404370b5a4b885d3e2a1f6567d314f8b4a41974ffe7d08e525bf92ae0f9c7030e3b4c78e4e34ace55d6a67a74f1571bc205959f5972f88f0 + languageName: node + linkType: hard + "supports-color@npm:^5.3.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -12434,7 +12819,7 @@ __metadata: languageName: node linkType: hard -"terser@npm:^5.26.0": +"terser@npm:^5.17.4, terser@npm:^5.26.0": version: 5.26.0 resolution: "terser@npm:5.26.0" dependencies: @@ -12635,6 +13020,13 @@ __metadata: languageName: node linkType: hard +"ts-interface-checker@npm:^0.1.9": + version: 0.1.13 + resolution: "ts-interface-checker@npm:0.1.13" + checksum: 20c29189c2dd6067a8775e07823ddf8d59a33e2ffc47a1bd59a5cb28bb0121a2969a816d5e77eda2ed85b18171aa5d1c4005a6b88ae8499ec7cc49f78571cb5e + languageName: node + linkType: hard + "ts-jest@npm:^28.0.5": version: 28.0.7 resolution: "ts-jest@npm:28.0.7" @@ -13293,6 +13685,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: ^4.0.0 + string-width: ^4.1.0 + strip-ansi: ^6.0.0 + checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b + languageName: node + linkType: hard + "wrap-ansi@npm:^6.2.0": version: 6.2.0 resolution: "wrap-ansi@npm:6.2.0" @@ -13304,14 +13707,14 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^7.0.0": - version: 7.0.0 - resolution: "wrap-ansi@npm:7.0.0" +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" dependencies: - ansi-styles: ^4.0.0 - string-width: ^4.1.0 - strip-ansi: ^6.0.0 - checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b + ansi-styles: ^6.1.0 + string-width: ^5.0.1 + strip-ansi: ^7.0.1 + checksum: 371733296dc2d616900ce15a0049dca0ef67597d6394c57347ba334393599e800bab03c41d4d45221b6bc967b8c453ec3ae4749eff3894202d16800fdfe0e238 languageName: node linkType: hard From e6c5ab97261882aa15274a0b697186099d923d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Fri, 12 Jan 2024 14:55:07 +0100 Subject: [PATCH 3/6] build(rulesets): abandon props mangling --- .eslintignore | 2 +- .gitignore | 2 +- packages/rulesets/package.json | 6 +- packages/rulesets/scripts/bundle.ts | 37 -- packages/rulesets/scripts/compile-schemas.ts | 28 +- .../src/oas/functions/_oasDocumentSchema.ts | 116 ----- .../src/oas/functions/oasDocumentSchema.ts | 108 ++++- .../src/oas/schemas/__fixtures__/validate.ts | 0 .../src/oas/schemas/{ => oas}/LICENSE | 0 yarn.lock | 443 +----------------- 10 files changed, 153 insertions(+), 589 deletions(-) delete mode 100644 packages/rulesets/scripts/bundle.ts delete mode 100644 packages/rulesets/src/oas/functions/_oasDocumentSchema.ts create mode 100644 packages/rulesets/src/oas/schemas/__fixtures__/validate.ts rename packages/rulesets/src/oas/schemas/{ => oas}/LICENSE (100%) diff --git a/.eslintignore b/.eslintignore index f12606a84..5c2348a5f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,6 @@ **/__fixtures__/** /test-harness/tests/ /packages/*/dist -/packages/rulesets/src/oas/schemas/compiled.ts +/packages/rulesets/src/oas/schemas/validators.ts /packages/*/CHANGELOG.md packages/formatters/src/html/templates.ts diff --git a/.gitignore b/.gitignore index 69365a2d2..0ad7557a3 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ node_modules !.yarn/versions packages/formatters/src/html/templates.ts -packages/rulesets/src/oas/schemas/compiled.ts +packages/rulesets/src/oas/schemas/validators.ts packages/cli/binaries packages/*/src/version.ts /test-harness/tmp/ diff --git a/packages/rulesets/package.json b/packages/rulesets/package.json index fca3d4a96..8f8483227 100644 --- a/packages/rulesets/package.json +++ b/packages/rulesets/package.json @@ -50,17 +50,15 @@ "tslib": "^2.3.0" }, "devDependencies": { - "@rollup/plugin-sucrase": "^5.0.2", - "@rollup/plugin-terser": "^0.4.4", "@stoplight/path": "^1.3.2", "@stoplight/spectral-parsers": "*", "@stoplight/spectral-ref-resolver": "*", "gzip-size": "^6.0.0", - "immer": "^9.0.6" + "immer": "^9.0.6", + "terser": "^5.26.0" }, "scripts": { "compile-schemas": "ts-node -T ./scripts/compile-schemas.ts", - "postbuild": "ts-node -T ./scripts/bundle.ts", "prelint": "yarn compile-schemas --quiet", "pretest": "yarn compile-schemas --quiet", "prebuild": "yarn compile-schemas --quiet" diff --git a/packages/rulesets/scripts/bundle.ts b/packages/rulesets/scripts/bundle.ts deleted file mode 100644 index 5c7cc886b..000000000 --- a/packages/rulesets/scripts/bundle.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { rollup } from 'rollup'; -import terser from '@rollup/plugin-terser'; -import * as path from 'path'; -import sucrase from '@rollup/plugin-sucrase'; - -const cwd = path.join(__dirname, '..'); - -rollup({ - input: path.join(cwd, 'src/oas/functions/_oasDocumentSchema.ts'), - plugins: [ - sucrase({ - transforms: ['typescript'], - }), - terser({ - ecma: 2020, - module: true, - compress: { - passes: 2, - }, - mangle: { - // properties: true, - // reserved: ['message', 'path'], - }, - }), - ], - treeshake: true, - watch: false, - perf: false, - external: ['@stoplight/spectral-core', '@stoplight/json', 'leven'], -}).then(bundle => - bundle.write({ - format: 'commonjs', - exports: 'default', - sourcemap: true, - file: path.join(cwd, 'dist/oas/functions/_oasDocumentSchema.js'), - }), -); diff --git a/packages/rulesets/scripts/compile-schemas.ts b/packages/rulesets/scripts/compile-schemas.ts index 4b354ff44..5c4169e57 100644 --- a/packages/rulesets/scripts/compile-schemas.ts +++ b/packages/rulesets/scripts/compile-schemas.ts @@ -7,6 +7,8 @@ import standaloneCode from 'ajv/dist/standalone/index.js'; import ajvErrors from 'ajv-errors'; import ajvFormats from 'ajv-formats'; import chalk from 'chalk'; +import { minify } from 'terser'; +import { sync } from 'gzip-size'; const cwd = path.join(__dirname, '../src'); @@ -39,14 +41,13 @@ Promise.all(schemas) code: { esm: true, source: true, - optimize: 1, }, }); ajvFormats(ajv); ajvErrors(ajv); - const target = path.join(cwd, 'oas/schemas/compiled.ts'); + const target = path.join(cwd, 'oas/schemas/validators.ts'); const basename = path.basename(target); const code = standaloneCode(ajv, { oas2_0: 'http://swagger.io/v2/schema.json', @@ -54,9 +55,28 @@ Promise.all(schemas) oas3_1: 'https://spec.openapis.org/oas/3.1/schema/2021-09-28', }); - log('writing %s size is %dKB', path.join(target, '..', basename), Math.round((code.length / 1024) * 100) / 100); + const minified = ( + await minify(code, { + compress: { + passes: 2, + }, + ecma: 2020, + module: true, + format: { + comments: false, + }, + }) + ).code as string; - await fs.promises.writeFile(path.join(target, '..', basename), ['// @ts-nocheck', code].join('\n')); + log( + 'writing %s size is %dKB (original), %dKB (minified) %dKB (minified + gzipped)', + path.join(target, '..', basename), + Math.round((code.length / 1024) * 100) / 100, + Math.round((minified.length / 1024) * 100) / 100, + Math.round((sync(minified) / 1024) * 100) / 100, + ); + + await fs.promises.writeFile(path.join(target, '..', basename), ['// @ts-nocheck', minified].join('\n')); }) .then(() => { log(chalk.green('Validators generated.')); diff --git a/packages/rulesets/src/oas/functions/_oasDocumentSchema.ts b/packages/rulesets/src/oas/functions/_oasDocumentSchema.ts deleted file mode 100644 index 68b2ea3f4..000000000 --- a/packages/rulesets/src/oas/functions/_oasDocumentSchema.ts +++ /dev/null @@ -1,116 +0,0 @@ -import type { IFunctionResult } from '@stoplight/spectral-core'; -import { isPlainObject, resolveInlineRef } from '@stoplight/json'; -import type { ErrorObject } from 'ajv'; -import leven from 'leven'; - -import { oas2_0, oas3_0, oas3_1 } from '../schemas/compiled'; - -function getValidator(format: 'oas2_0' | 'oas3_0' | 'oas3_1'): typeof oas2_0 | typeof oas3_0 | typeof oas3_1 { - switch (format) { - case 'oas2_0': - return oas2_0; - case 'oas3_0': - return oas3_0; - case 'oas3_1': - return oas3_1; - } -} - -function isRelevantError(error: ErrorObject): boolean { - return error.keyword !== 'if'; -} - -export default function (format: 'oas2_0' | 'oas3_0' | 'oas3_1', input: unknown): IFunctionResult[] | void { - const validator = getValidator(format); - validator(input); - - // @ts-expect-error: validator typings aren't fully correct - const errors = validator.errors as ErrorObject[] | undefined; - - return errors?.filter(isRelevantError).map(e => processError(input, e)); -} - -function processError(input: unknown, error: ErrorObject): IFunctionResult { - const path = error.instancePath === '' ? [] : error.instancePath.slice(1).split('/'); - const property = path.length === 0 ? null : path[path.length - 1]; - - switch (error.keyword) { - case 'additionalProperties': { - const additionalProperty = error.params['additionalProperty'] as string; - path.push(additionalProperty); - - return { - message: `Property "${additionalProperty}" is not expected to be here`, - path, - }; - } - - case 'enum': { - const allowedValues = error.params['allowedValues'] as unknown[]; - const printedValues = allowedValues.map(value => JSON.stringify(value)).join(', '); - let suggestion: string; - - if (!isPlainObject(input)) { - suggestion = ''; - } else { - const value = resolveInlineRef(input, `#${error.instancePath}`); - if (typeof value !== 'string') { - suggestion = ''; - } else { - const bestMatch = findBestMatch(value, allowedValues); - - if (bestMatch !== null) { - suggestion = `. Did you mean "${bestMatch}"?`; - } else { - suggestion = ''; - } - } - } - - return { - message: `${cleanAjvMessage(property, error.message)}: ${printedValues}${suggestion}`, - path, - }; - } - - case 'errorMessage': - return { - message: String(error.message), - path, - }; - - default: - return { - message: cleanAjvMessage(property, error.message), - path, - }; - } -} - -function findBestMatch(value: string, allowedValues: unknown[]): string | null { - const matches = allowedValues - .filter((value): value is string => typeof value === 'string') - .map(allowedValue => ({ - value: allowedValue, - weight: leven(value, allowedValue), - })) - .sort((x, y) => (x.weight > y.weight ? 1 : x.weight < y.weight ? -1 : 0)); - - if (matches.length === 0) { - return null; - } - - const bestMatch = matches[0]; - - return allowedValues.length === 1 || bestMatch.weight < bestMatch.value.length ? bestMatch.value : null; -} - -const QUOTES = /['"]/g; -const NOT = /NOT/g; - -function cleanAjvMessage(prop: string | null, message: string | undefined): string { - if (typeof message !== 'string') return ''; - - const cleanedMessage = message.replace(QUOTES, '"').replace(NOT, 'not'); - return prop === null ? cleanedMessage : `"${prop}" property ${cleanedMessage}`; -} diff --git a/packages/rulesets/src/oas/functions/oasDocumentSchema.ts b/packages/rulesets/src/oas/functions/oasDocumentSchema.ts index 8c5397365..cf349ad39 100644 --- a/packages/rulesets/src/oas/functions/oasDocumentSchema.ts +++ b/packages/rulesets/src/oas/functions/oasDocumentSchema.ts @@ -1,6 +1,11 @@ import { createRulesetFunction } from '@stoplight/spectral-core'; +import type { IFunctionResult } from '@stoplight/spectral-core'; import { oas2, oas3_1 } from '@stoplight/spectral-formats'; -import _oasDocumentSchema from './_oasDocumentSchema'; +import { isPlainObject, resolveInlineRef } from '@stoplight/json'; +import type { ErrorObject } from 'ajv'; +import leven from 'leven'; + +import * as validators from '../schemas/validators'; export default createRulesetFunction( { @@ -11,8 +16,105 @@ export default createRulesetFunction( const formats = context.document.formats; if (formats === null || formats === void 0) return; - const format = formats.has(oas2) ? 'oas2_0' : formats.has(oas3_1) ? 'oas3_1' : 'oas3_0'; + const validator = formats.has(oas2) + ? validators.oas2_0 + : formats.has(oas3_1) + ? validators.oas3_1 + : validators.oas3_0; + + validator(input); + + const errors = validator['errors'] as ErrorObject[] | undefined; - return _oasDocumentSchema(format, input); + return errors?.filter(isRelevantError).map(e => processError(input, e)); }, ); + +function isRelevantError(error: ErrorObject): boolean { + return error.keyword !== 'if'; +} + +function processError(input: unknown, error: ErrorObject): IFunctionResult { + const path = error.instancePath === '' ? [] : error.instancePath.slice(1).split('/'); + const property = path.length === 0 ? null : path[path.length - 1]; + + switch (error.keyword) { + case 'additionalProperties': { + const additionalProperty = error.params['additionalProperty'] as string; + path.push(additionalProperty); + + return { + message: `Property "${additionalProperty}" is not expected to be here`, + path, + }; + } + + case 'enum': { + const allowedValues = error.params['allowedValues'] as unknown[]; + const printedValues = allowedValues.map(value => JSON.stringify(value)).join(', '); + let suggestion: string; + + if (!isPlainObject(input)) { + suggestion = ''; + } else { + const value = resolveInlineRef(input, `#${error.instancePath}`); + if (typeof value !== 'string') { + suggestion = ''; + } else { + const bestMatch = findBestMatch(value, allowedValues); + + if (bestMatch !== null) { + suggestion = `. Did you mean "${bestMatch}"?`; + } else { + suggestion = ''; + } + } + } + + return { + message: `${cleanAjvMessage(property, error.message)}: ${printedValues}${suggestion}`, + path, + }; + } + + case 'errorMessage': + return { + message: String(error.message), + path, + }; + + default: + return { + message: cleanAjvMessage(property, error.message), + path, + }; + } +} + +function findBestMatch(value: string, allowedValues: unknown[]): string | null { + const matches = allowedValues + .filter((value): value is string => typeof value === 'string') + .map(allowedValue => ({ + value: allowedValue, + weight: leven(value, allowedValue), + })) + .sort((x, y) => (x.weight > y.weight ? 1 : x.weight < y.weight ? -1 : 0)); + + if (matches.length === 0) { + return null; + } + + const bestMatch = matches[0]; + + return allowedValues.length === 1 || bestMatch.weight < bestMatch.value.length ? bestMatch.value : null; +} + +const QUOTES = /['"]/g; +const NOT = /NOT/g; + +function cleanAjvMessage(prop: string | null, message: string | undefined): string { + if (typeof message !== 'string') return ''; + + const cleanedMessage = message.replace(QUOTES, '"').replace(NOT, 'not'); + return prop === null ? cleanedMessage : `"${prop}" property ${cleanedMessage}`; +} diff --git a/packages/rulesets/src/oas/schemas/__fixtures__/validate.ts b/packages/rulesets/src/oas/schemas/__fixtures__/validate.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/rulesets/src/oas/schemas/LICENSE b/packages/rulesets/src/oas/schemas/oas/LICENSE similarity index 100% rename from packages/rulesets/src/oas/schemas/LICENSE rename to packages/rulesets/src/oas/schemas/oas/LICENSE diff --git a/yarn.lock b/yarn.lock index fc49ab2fc..f2fb6dfc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1466,20 +1466,6 @@ __metadata: languageName: node linkType: hard -"@isaacs/cliui@npm:^8.0.2": - version: 8.0.2 - resolution: "@isaacs/cliui@npm:8.0.2" - dependencies: - string-width: ^5.1.2 - string-width-cjs: "npm:string-width@^4.2.0" - strip-ansi: ^7.0.1 - strip-ansi-cjs: "npm:strip-ansi@^6.0.1" - wrap-ansi: ^8.1.0 - wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" - checksum: 4a473b9b32a7d4d3cfb7a614226e555091ff0c5a29a1734c28c72a182c2f6699b26fc6b5c2131dfd841e86b185aea714c72201d7c98c2fba5f17709333a67aeb - languageName: node - linkType: hard - "@isaacs/string-locale-compare@npm:^1.1.0": version: 1.1.0 resolution: "@isaacs/string-locale-compare@npm:1.1.0" @@ -2354,13 +2340,6 @@ __metadata: languageName: node linkType: hard -"@pkgjs/parseargs@npm:^0.11.0": - version: 0.11.0 - resolution: "@pkgjs/parseargs@npm:0.11.0" - checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f - languageName: node - linkType: hard - "@pnpm/config.env-replace@npm:^1.1.0": version: 1.1.0 resolution: "@pnpm/config.env-replace@npm:1.1.0" @@ -2405,37 +2384,6 @@ __metadata: languageName: node linkType: hard -"@rollup/plugin-sucrase@npm:^5.0.2": - version: 5.0.2 - resolution: "@rollup/plugin-sucrase@npm:5.0.2" - dependencies: - "@rollup/pluginutils": ^5.0.1 - sucrase: ^3.27.0 - peerDependencies: - rollup: ^2.53.1||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - checksum: 3780cc4a6e3cc492a25e0eb0efb2f99e353f1c05497c97d52c901f07ef262ea114de312cba3a6b6d86cca49e28817d85e4bbc9b060c32f94d7364d8c8f2198a7 - languageName: node - linkType: hard - -"@rollup/plugin-terser@npm:^0.4.4": - version: 0.4.4 - resolution: "@rollup/plugin-terser@npm:0.4.4" - dependencies: - serialize-javascript: ^6.0.1 - smob: ^1.0.0 - terser: ^5.17.4 - peerDependencies: - rollup: ^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - checksum: 5472f659fbb7034488df91eb01ecd2ddf6d2cf203d049aa486139225ad5566254c6ec24aad1f5d1167e35f480212ede5160df9cc80e149a28874f78ed6a7fd9a - languageName: node - linkType: hard - "@rollup/pluginutils@npm:^3.1.0": version: 3.1.0 resolution: "@rollup/pluginutils@npm:3.1.0" @@ -2449,22 +2397,6 @@ __metadata: languageName: node linkType: hard -"@rollup/pluginutils@npm:^5.0.1": - version: 5.1.0 - resolution: "@rollup/pluginutils@npm:5.1.0" - dependencies: - "@types/estree": ^1.0.0 - estree-walker: ^2.0.2 - picomatch: ^2.3.1 - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - checksum: 3cc5a6d91452a6eabbfd1ae79b4dd1f1e809d2eecda6e175deb784e75b0911f47e9ecce73f8dd315d6a8b3f362582c91d3c0f66908b6ced69345b3cbe28f8ce8 - languageName: node - linkType: hard - "@semantic-release/changelog@npm:^6.0.3": version: 6.0.3 resolution: "@semantic-release/changelog@npm:6.0.3" @@ -2905,8 +2837,6 @@ __metadata: resolution: "@stoplight/spectral-rulesets@workspace:packages/rulesets" dependencies: "@asyncapi/specs": ^4.1.0 - "@rollup/plugin-sucrase": ^5.0.2 - "@rollup/plugin-terser": ^0.4.4 "@stoplight/better-ajv-errors": 1.0.3 "@stoplight/json": ^3.17.0 "@stoplight/path": ^1.3.2 @@ -2925,8 +2855,6 @@ __metadata: json-schema-traverse: ^1.0.0 leven: 3.1.0 lodash: ~4.17.21 - rollup-plugin-bundle-size: ^1.0.3 - rollup-plugin-output-size: ^1.3.0 terser: ^5.26.0 tslib: ^2.3.0 languageName: unknown @@ -3254,13 +3182,6 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:^1.0.0": - version: 1.0.5 - resolution: "@types/estree@npm:1.0.5" - checksum: dd8b5bed28e6213b7acd0fb665a84e693554d850b0df423ac8076cc3ad5823a6bc26b0251d080bdc545af83179ede51dd3f6fa78cad2c46ed1f29624ddf3e41a - languageName: node - linkType: hard - "@types/file-entry-cache@npm:^5.0.2": version: 5.0.2 resolution: "@types/file-entry-cache@npm:5.0.2" @@ -3865,20 +3786,6 @@ __metadata: languageName: node linkType: hard -"ansi-regex@npm:^6.0.1": - version: 6.0.1 - resolution: "ansi-regex@npm:6.0.1" - checksum: 1ff8b7667cded1de4fa2c9ae283e979fc87036864317da86a2e546725f96406746411d0d85e87a2d12fa5abd715d90006de7fa4fa0477c92321ad3b4c7d4e169 - languageName: node - linkType: hard - -"ansi-styles@npm:^2.2.1": - version: 2.2.1 - resolution: "ansi-styles@npm:2.2.1" - checksum: ebc0e00381f2a29000d1dac8466a640ce11943cef3bda3cd0020dc042e31e1058ab59bf6169cd794a54c3a7338a61ebc404b7c91e004092dd20e028c432c9c2c - languageName: node - linkType: hard - "ansi-styles@npm:^3.2.1": version: 3.2.1 resolution: "ansi-styles@npm:3.2.1" @@ -3904,13 +3811,6 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.1.0": - version: 6.2.1 - resolution: "ansi-styles@npm:6.2.1" - checksum: ef940f2f0ced1a6347398da88a91da7930c33ecac3c77b72c5905f8b8fe402c52e6fde304ff5347f616e27a742da3f1dc76de98f6866c69251ad0b07a66776d9 - languageName: node - linkType: hard - "ansicolors@npm:~0.3.2": version: 0.3.2 resolution: "ansicolors@npm:0.3.2" @@ -4668,19 +4568,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^1.0.0, chalk@npm:^1.1.3": - version: 1.1.3 - resolution: "chalk@npm:1.1.3" - dependencies: - ansi-styles: ^2.2.1 - escape-string-regexp: ^1.0.2 - has-ansi: ^2.0.0 - strip-ansi: ^3.0.0 - supports-color: ^2.0.0 - checksum: 9d2ea6b98fc2b7878829eec223abcf404622db6c48396a9b9257f6d0ead2acf18231ae368d6a664a83f272b0679158da12e97b5229f794939e555cc574478acd - languageName: node - linkType: hard - "chalk@npm:^2.3.2, chalk@npm:^2.4.1, chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -4949,13 +4836,6 @@ __metadata: languageName: node linkType: hard -"colorette@npm:^2.0.20": - version: 2.0.20 - resolution: "colorette@npm:2.0.20" - checksum: 0c016fea2b91b733eb9f4bcdb580018f52c0bc0979443dad930e5037a968237ac53d9beb98e218d2e9235834f8eebce7f8e080422d6194e957454255bde71d3d - languageName: node - linkType: hard - "columnify@npm:^1.6.0": version: 1.6.0 resolution: "columnify@npm:1.6.0" @@ -4994,13 +4874,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^4.0.0": - version: 4.1.1 - resolution: "commander@npm:4.1.1" - checksum: d7b9913ff92cae20cb577a4ac6fcc121bd6223319e54a40f51a14740a681ad5c574fd29a57da478a5f234a6fa6c52cbf0b7c641353e03c648b1ae85ba670b977 - languageName: node - linkType: hard - "commander@npm:^8.2.0": version: 8.2.0 resolution: "commander@npm:8.2.0" @@ -5749,20 +5622,13 @@ __metadata: languageName: node linkType: hard -"duplexer@npm:^0.1.1, duplexer@npm:^0.1.2": +"duplexer@npm:^0.1.2": version: 0.1.2 resolution: "duplexer@npm:0.1.2" checksum: 62ba61a830c56801db28ff6305c7d289b6dc9f859054e8c982abd8ee0b0a14d2e9a8e7d086ffee12e868d43e2bbe8a964be55ddbd8c8957714c87373c7a4f9b0 languageName: node linkType: hard -"eastasianwidth@npm:^0.2.0": - version: 0.2.0 - resolution: "eastasianwidth@npm:0.2.0" - checksum: 7d00d7cd8e49b9afa762a813faac332dee781932d6f2c848dc348939c4253f1d4564341b7af1d041853bc3f32c2ef141b58e0a4d9862c17a7f08f68df1e0f1ed - languageName: node - linkType: hard - "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -5806,13 +5672,6 @@ __metadata: languageName: node linkType: hard -"emoji-regex@npm:^9.2.2": - version: 9.2.2 - resolution: "emoji-regex@npm:9.2.2" - checksum: 8487182da74aabd810ac6d6f1994111dfc0e331b01271ae01ec1eb0ad7b5ecc2bbbbd2f053c05cb55a1ac30449527d819bbfbf0e3de1023db308cbcb47f86601 - languageName: node - linkType: hard - "encodeurl@npm:~1.0.2": version: 1.0.2 resolution: "encodeurl@npm:1.0.2" @@ -6045,7 +5904,7 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:^1.0.2, escape-string-regexp@npm:^1.0.5": +"escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" checksum: 6092fda75c63b110c706b6a9bfde8a612ad595b628f0bd2147eea1d3406723020810e591effc7db1da91d80a71a737a313567c5abb3813e8d9c71f4aa595b410 @@ -6289,7 +6148,7 @@ __metadata: languageName: node linkType: hard -"estree-walker@npm:^2.0.1, estree-walker@npm:^2.0.2": +"estree-walker@npm:^2.0.1": version: 2.0.2 resolution: "estree-walker@npm:2.0.2" checksum: 6151e6f9828abe2259e57f5fd3761335bb0d2ebd76dc1a01048ccee22fabcfef3c0859300f6d83ff0d1927849368775ec5a6d265dde2f6de5a1be1721cd94efc @@ -6530,16 +6389,6 @@ __metadata: languageName: node linkType: hard -"figures@npm:^1.0.1": - version: 1.7.0 - resolution: "figures@npm:1.7.0" - dependencies: - escape-string-regexp: ^1.0.5 - object-assign: ^4.1.0 - checksum: d77206deba991a7977f864b8c8edf9b8b43b441be005482db04b0526e36263adbdb22c1c6d2df15a1ad78d12029bd1aa41ccebcb5d425e1f2cf629c6daaa8e10 - languageName: node - linkType: hard - "figures@npm:^2.0.0": version: 2.0.0 resolution: "figures@npm:2.0.0" @@ -6665,16 +6514,6 @@ __metadata: languageName: node linkType: hard -"foreground-child@npm:^3.1.0": - version: 3.1.1 - resolution: "foreground-child@npm:3.1.1" - dependencies: - cross-spawn: ^7.0.0 - signal-exit: ^4.0.1 - checksum: 139d270bc82dc9e6f8bc045fe2aae4001dc2472157044fdfad376d0a3457f77857fa883c1c8b21b491c6caade9a926a4bed3d3d2e8d3c9202b151a4cbbd0bcd5 - languageName: node - linkType: hard - "form-data@npm:^3.0.0": version: 3.0.0 resolution: "form-data@npm:3.0.0" @@ -6997,21 +6836,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.3.10": - version: 10.3.10 - resolution: "glob@npm:10.3.10" - dependencies: - foreground-child: ^3.1.0 - jackspeak: ^2.3.5 - minimatch: ^9.0.1 - minipass: ^5.0.0 || ^6.0.2 || ^7.0.0 - path-scurry: ^1.10.1 - bin: - glob: dist/esm/bin.mjs - checksum: 4f2fe2511e157b5a3f525a54092169a5f92405f24d2aed3142f4411df328baca13059f4182f1db1bf933e2c69c0bd89e57ae87edd8950cba8c7ccbe84f721cf3 - languageName: node - linkType: hard - "glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -7122,15 +6946,6 @@ __metadata: languageName: node linkType: hard -"gzip-size@npm:^3.0.0": - version: 3.0.0 - resolution: "gzip-size@npm:3.0.0" - dependencies: - duplexer: ^0.1.1 - checksum: 683095068fc28e5dfa7dd77114ba95583d5acfd99e8028a993602e620eb9d48bf7910c14a3117caa9d665e3e1271b4027396f714be30f2b619dc638c76e5a6e8 - languageName: node - linkType: hard - "gzip-size@npm:^6.0.0": version: 6.0.0 resolution: "gzip-size@npm:6.0.0" @@ -7140,15 +6955,6 @@ __metadata: languageName: node linkType: hard -"gzip-size@npm:^7.0.0": - version: 7.0.0 - resolution: "gzip-size@npm:7.0.0" - dependencies: - duplexer: ^0.1.2 - checksum: 52d0bf586307082428b99f7b04d56d756d640e1f84d4a56debf9fb8c972d9db679143b067dd4024ebef42e9f6787e9dc8b9dcad344372b9dc87e55d942276f49 - languageName: node - linkType: hard - "handlebars@npm:^4.7.7": version: 4.7.7 resolution: "handlebars@npm:4.7.7" @@ -7174,15 +6980,6 @@ __metadata: languageName: node linkType: hard -"has-ansi@npm:^2.0.0": - version: 2.0.0 - resolution: "has-ansi@npm:2.0.0" - dependencies: - ansi-regex: ^2.0.0 - checksum: 1b51daa0214440db171ff359d0a2d17bc20061164c57e76234f614c91dbd2a79ddd68dfc8ee73629366f7be45a6df5f2ea9de83f52e1ca24433f2cc78c35d8ec - languageName: node - linkType: hard - "has-bigints@npm:^1.0.1": version: 1.0.1 resolution: "has-bigints@npm:1.0.1" @@ -8052,19 +7849,6 @@ __metadata: languageName: node linkType: hard -"jackspeak@npm:^2.3.5": - version: 2.3.6 - resolution: "jackspeak@npm:2.3.6" - dependencies: - "@isaacs/cliui": ^8.0.2 - "@pkgjs/parseargs": ^0.11.0 - dependenciesMeta: - "@pkgjs/parseargs": - optional: true - checksum: 57d43ad11eadc98cdfe7496612f6bbb5255ea69fe51ea431162db302c2a11011642f50cfad57288bd0aea78384a0612b16e131944ad8ecd09d619041c8531b54 - languageName: node - linkType: hard - "jasmine-core@npm:^4.1.0": version: 4.2.0 resolution: "jasmine-core@npm:4.2.0" @@ -9339,13 +9123,6 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^9.1.1 || ^10.0.0": - version: 10.1.0 - resolution: "lru-cache@npm:10.1.0" - checksum: 58056d33e2500fbedce92f8c542e7c11b50d7d086578f14b7074d8c241422004af0718e08a6eaae8705cee09c77e39a61c1c79e9370ba689b7010c152e6a76ab - languageName: node - linkType: hard - "lru-queue@npm:^0.1.0": version: 0.1.0 resolution: "lru-queue@npm:0.1.0" @@ -9476,18 +9253,6 @@ __metadata: languageName: node linkType: hard -"maxmin@npm:^2.1.0": - version: 2.1.0 - resolution: "maxmin@npm:2.1.0" - dependencies: - chalk: ^1.0.0 - figures: ^1.0.1 - gzip-size: ^3.0.0 - pretty-bytes: ^3.0.0 - checksum: 97e2377454c4b436df8cfe46cff95e8e6166a69b5256a6513d4afc3468eeee3d26eaaac153d26c7e7cef1f775c28c7d58b4399929d5472801b666a99581d0fdb - languageName: node - linkType: hard - "md5.js@npm:^1.3.4": version: 1.3.5 resolution: "md5.js@npm:1.3.5" @@ -9673,15 +9438,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.1": - version: 9.0.3 - resolution: "minimatch@npm:9.0.3" - dependencies: - brace-expansion: ^2.0.1 - checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 - languageName: node - linkType: hard - "minimist-options@npm:4.1.0": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" @@ -9792,13 +9548,6 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0": - version: 7.0.4 - resolution: "minipass@npm:7.0.4" - checksum: 87585e258b9488caf2e7acea242fd7856bbe9a2c84a7807643513a338d66f368c7d518200ad7b70a508664d408aa000517647b2930c259a8b1f9f0984f344a21 - languageName: node - linkType: hard - "minizlib@npm:^2.0.0, minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": version: 2.1.2 resolution: "minizlib@npm:2.1.2" @@ -10903,16 +10652,6 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^1.10.1": - version: 1.10.1 - resolution: "path-scurry@npm:1.10.1" - dependencies: - lru-cache: ^9.1.1 || ^10.0.0 - minipass: ^5.0.0 || ^6.0.2 || ^7.0.0 - checksum: e2557cff3a8fb8bc07afdd6ab163a92587884f9969b05bbbaf6fe7379348bfb09af9ed292af12ed32398b15fb443e81692047b786d1eeb6d898a51eb17ed7d90 - languageName: node - linkType: hard - "path-to-regexp@npm:^2.2.1": version: 2.4.0 resolution: "path-to-regexp@npm:2.4.0" @@ -10954,13 +10693,6 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.3.1": - version: 2.3.1 - resolution: "picomatch@npm:2.3.1" - checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf - languageName: node - linkType: hard - "pify@npm:^3.0.0": version: 3.0.0 resolution: "pify@npm:3.0.0" @@ -10975,13 +10707,6 @@ __metadata: languageName: node linkType: hard -"pirates@npm:^4.0.1": - version: 4.0.6 - resolution: "pirates@npm:4.0.6" - checksum: 46a65fefaf19c6f57460388a5af9ab81e3d7fd0e7bc44ca59d753cb5c4d0df97c6c6e583674869762101836d68675f027d60f841c105d72734df9dfca97cbcc6 - languageName: node - linkType: hard - "pirates@npm:^4.0.4": version: 4.0.5 resolution: "pirates@npm:4.0.5" @@ -11081,22 +10806,6 @@ __metadata: languageName: node linkType: hard -"pretty-bytes@npm:^3.0.0": - version: 3.0.1 - resolution: "pretty-bytes@npm:3.0.1" - dependencies: - number-is-nan: ^1.0.0 - checksum: 0709a19bb30c0a35d84f2afdfbeaef3e68703c28346e85413493edd687f7509d1ec987cda2fe54554b9481426ba775f4cd6108c16633353768cfad4d417baacd - languageName: node - linkType: hard - -"pretty-bytes@npm:^6.1.1": - version: 6.1.1 - resolution: "pretty-bytes@npm:6.1.1" - checksum: 43d29d909d2d88072da2c3d72f8fd0f2d2523c516bfa640aff6e31f596ea1004b6601f4cabc50d14b2cf10e82635ebe5b7d9378f3d5bae1c0067131829421b8a - languageName: node - linkType: hard - "pretty-format@npm:^27.5.1": version: 27.5.1 resolution: "pretty-format@npm:27.5.1" @@ -11315,7 +11024,7 @@ __metadata: languageName: node linkType: hard -"randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5, randombytes@npm:^2.1.0": +"randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5": version: 2.1.0 resolution: "randombytes@npm:2.1.0" dependencies: @@ -11763,32 +11472,6 @@ __metadata: languageName: node linkType: hard -"rollup-plugin-bundle-size@npm:^1.0.3": - version: 1.0.3 - resolution: "rollup-plugin-bundle-size@npm:1.0.3" - dependencies: - chalk: ^1.1.3 - maxmin: ^2.1.0 - checksum: 21165474bbac68484c98e4a6346888511dca327da3d9b9d7ab15cb003c67a052443d8a599fb5647b7a312104d2740f246ba9b692754dda92be2a20d5f7fc4fd6 - languageName: node - linkType: hard - -"rollup-plugin-output-size@npm:^1.3.0": - version: 1.3.0 - resolution: "rollup-plugin-output-size@npm:1.3.0" - dependencies: - colorette: ^2.0.20 - gzip-size: ^7.0.0 - pretty-bytes: ^6.1.1 - peerDependencies: - rollup: ^2.0.0 || ^3.0.0 || ^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - checksum: 47bda6876f9d12e5eccccc61c401ee624b511a3a8ffe2b75b597105603c77af653e4bb714918bcd7763a21fa68fb27e2caca2afe7cbece2e53fb564f7a895f71 - languageName: node - linkType: hard - "rollup@npm:~2.79.0": version: 2.79.0 resolution: "rollup@npm:2.79.0" @@ -12018,15 +11701,6 @@ __metadata: languageName: node linkType: hard -"serialize-javascript@npm:^6.0.1": - version: 6.0.2 - resolution: "serialize-javascript@npm:6.0.2" - dependencies: - randombytes: ^2.1.0 - checksum: c4839c6206c1d143c0f80763997a361310305751171dd95e4b57efee69b8f6edd8960a0b7fbfc45042aadff98b206d55428aee0dc276efe54f100899c7fa8ab7 - languageName: node - linkType: hard - "set-blocking@npm:^2.0.0, set-blocking@npm:~2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -12119,13 +11793,6 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1": - version: 4.1.0 - resolution: "signal-exit@npm:4.1.0" - checksum: 64c757b498cb8629ffa5f75485340594d2f8189e9b08700e69199069c8e3070fb3e255f7ab873c05dc0b3cec412aea7402e10a5990cb6a050bd33ba062a6c549 - languageName: node - linkType: hard - "signale@npm:^1.2.1, signale@npm:^1.4.0": version: 1.4.0 resolution: "signale@npm:1.4.0" @@ -12214,13 +11881,6 @@ __metadata: languageName: node linkType: hard -"smob@npm:^1.0.0": - version: 1.4.1 - resolution: "smob@npm:1.4.1" - checksum: 3bd9e6bcc440356b0e06165f04f0ea170ebc1d57713e4a1d64c57227cb423d8346d3e0894fd7ce28bf75958f73a62f91ba13574a9a0fb4cbc271fa9ef5d75f4e - languageName: node - linkType: hard - "socket.io-adapter@npm:~2.4.0": version: 2.4.0 resolution: "socket.io-adapter@npm:2.4.0" @@ -12528,17 +12188,6 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": - version: 4.2.3 - resolution: "string-width@npm:4.2.3" - dependencies: - emoji-regex: ^8.0.0 - is-fullwidth-code-point: ^3.0.0 - strip-ansi: ^6.0.1 - checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb - languageName: node - linkType: hard - "string-width@npm:^1.0.1": version: 1.0.2 resolution: "string-width@npm:1.0.2" @@ -12550,14 +12199,14 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^5.0.1, string-width@npm:^5.1.2": - version: 5.1.2 - resolution: "string-width@npm:5.1.2" +"string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" dependencies: - eastasianwidth: ^0.2.0 - emoji-regex: ^9.2.2 - strip-ansi: ^7.0.1 - checksum: 7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193 + emoji-regex: ^8.0.0 + is-fullwidth-code-point: ^3.0.0 + strip-ansi: ^6.0.1 + checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb languageName: node linkType: hard @@ -12610,7 +12259,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:6.0, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": +"strip-ansi@npm:6.0, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" dependencies: @@ -12628,15 +12277,6 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^7.0.1": - version: 7.1.0 - resolution: "strip-ansi@npm:7.1.0" - dependencies: - ansi-regex: ^6.0.1 - checksum: 859c73fcf27869c22a4e4d8c6acfe690064659e84bef9458aa6d13719d09ca88dcfd40cbf31fd0be63518ea1a643fe070b4827d353e09533a5b0b9fd4553d64d - languageName: node - linkType: hard - "strip-bom@npm:^3.0.0": version: 3.0.0 resolution: "strip-bom@npm:3.0.0" @@ -12681,24 +12321,6 @@ __metadata: languageName: node linkType: hard -"sucrase@npm:^3.27.0": - version: 3.35.0 - resolution: "sucrase@npm:3.35.0" - dependencies: - "@jridgewell/gen-mapping": ^0.3.2 - commander: ^4.0.0 - glob: ^10.3.10 - lines-and-columns: ^1.1.6 - mz: ^2.7.0 - pirates: ^4.0.1 - ts-interface-checker: ^0.1.9 - bin: - sucrase: bin/sucrase - sucrase-node: bin/sucrase-node - checksum: 9fc5792a9ab8a14dcf9c47dcb704431d35c1cdff1d17d55d382a31c2e8e3063870ad32ce120a80915498486246d612e30cda44f1624d9d9a10423e1a43487ad1 - languageName: node - linkType: hard - "supports-color@npm:8.1.1, supports-color@npm:^8.0.0": version: 8.1.1 resolution: "supports-color@npm:8.1.1" @@ -12708,13 +12330,6 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^2.0.0": - version: 2.0.0 - resolution: "supports-color@npm:2.0.0" - checksum: 602538c5812b9006404370b5a4b885d3e2a1f6567d314f8b4a41974ffe7d08e525bf92ae0f9c7030e3b4c78e4e34ace55d6a67a74f1571bc205959f5972f88f0 - languageName: node - linkType: hard - "supports-color@npm:^5.3.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -12819,7 +12434,7 @@ __metadata: languageName: node linkType: hard -"terser@npm:^5.17.4, terser@npm:^5.26.0": +"terser@npm:^5.26.0": version: 5.26.0 resolution: "terser@npm:5.26.0" dependencies: @@ -13020,13 +12635,6 @@ __metadata: languageName: node linkType: hard -"ts-interface-checker@npm:^0.1.9": - version: 0.1.13 - resolution: "ts-interface-checker@npm:0.1.13" - checksum: 20c29189c2dd6067a8775e07823ddf8d59a33e2ffc47a1bd59a5cb28bb0121a2969a816d5e77eda2ed85b18171aa5d1c4005a6b88ae8499ec7cc49f78571cb5e - languageName: node - linkType: hard - "ts-jest@npm:^28.0.5": version: 28.0.7 resolution: "ts-jest@npm:28.0.7" @@ -13685,17 +13293,6 @@ __metadata: languageName: node linkType: hard -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": - version: 7.0.0 - resolution: "wrap-ansi@npm:7.0.0" - dependencies: - ansi-styles: ^4.0.0 - string-width: ^4.1.0 - strip-ansi: ^6.0.0 - checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b - languageName: node - linkType: hard - "wrap-ansi@npm:^6.2.0": version: 6.2.0 resolution: "wrap-ansi@npm:6.2.0" @@ -13707,14 +13304,14 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^8.1.0": - version: 8.1.0 - resolution: "wrap-ansi@npm:8.1.0" +"wrap-ansi@npm:^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" dependencies: - ansi-styles: ^6.1.0 - string-width: ^5.0.1 - strip-ansi: ^7.0.1 - checksum: 371733296dc2d616900ce15a0049dca0ef67597d6394c57347ba334393599e800bab03c41d4d45221b6bc967b8c453ec3ae4749eff3894202d16800fdfe0e238 + ansi-styles: ^4.0.0 + string-width: ^4.1.0 + strip-ansi: ^6.0.0 + checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b languageName: node linkType: hard From c8124e12d278607001f494efbd1df220c9ccf0a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Fri, 12 Jan 2024 17:00:35 +0100 Subject: [PATCH 4/6] feat(rulesets): improve oas3.1 schema --- package.json | 4 +- packages/rulesets/package.json | 1 + packages/rulesets/scripts/compile-schemas.ts | 4 +- .../src/oas/__tests__/oas3-schema.test.ts | 289 +++++++++++++++--- .../src/oas/functions/oasDocumentSchema.ts | 55 ++-- .../src/oas/schemas/json-schema/LICENSE | 21 ++ .../src/oas/schemas/json-schema/README.md | 2 + .../draft-04.json} | 0 .../json-schema/draft-2020-12/index.json | 56 ++++ .../json-schema/draft-2020-12/validation.json | 102 +++++++ .../rulesets/src/oas/schemas/oas/v2.0.json | 56 ++-- .../rulesets/src/oas/schemas/oas/v3.0.json | 20 +- .../oas/schemas/oas/v3.1/dialect.schema.json | 25 +- .../src/oas/schemas/oas/v3.1/index.json | 3 +- .../src/oas/schemas/oas/v3.1/meta.schema.json | 30 +- yarn.lock | 38 +++ 16 files changed, 587 insertions(+), 119 deletions(-) create mode 100644 packages/rulesets/src/oas/schemas/json-schema/LICENSE create mode 100644 packages/rulesets/src/oas/schemas/json-schema/README.md rename packages/rulesets/src/oas/schemas/{json-schema-draft-04.json => json-schema/draft-04.json} (100%) create mode 100644 packages/rulesets/src/oas/schemas/json-schema/draft-2020-12/index.json create mode 100644 packages/rulesets/src/oas/schemas/json-schema/draft-2020-12/validation.json diff --git a/package.json b/package.json index e93a3febb..d99759294 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "lint": "yarn prelint && 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/ruleset/meta/*.json packages/rulesets/src/{asyncapi,oas}/schemas/*.json docs/**/*.md README.md", + "lint.prettier": "prettier --ignore-path .eslintignore --ignore-unknown --check packages/core/src/ruleset/meta/*.json packages/rulesets/src/{asyncapi,oas}/schemas/**/*.json docs/**/*.md README.md", "pretest": "yarn workspaces foreach run pretest", "test": "yarn pretest && yarn test.karma && yarn test.jest", "pretest.harness": "ts-node -T test-harness/scripts/generate-tests.ts", @@ -138,7 +138,7 @@ "packages/core/src/ruleset/meta/*.json": [ "prettier --ignore-path .eslintignore --write" ], - "packages/rulesets/src/{asyncapi,oas}/schemas/*.json": [ + "packages/rulesets/src/{asyncapi,oas}/schemas/**/*.json": [ "prettier --ignore-path .eslintignore --write" ] }, diff --git a/packages/rulesets/package.json b/packages/rulesets/package.json index 8f8483227..47ea02b23 100644 --- a/packages/rulesets/package.json +++ b/packages/rulesets/package.json @@ -53,6 +53,7 @@ "@stoplight/path": "^1.3.2", "@stoplight/spectral-parsers": "*", "@stoplight/spectral-ref-resolver": "*", + "ajv-merge-patch": "^5.0.1", "gzip-size": "^6.0.0", "immer": "^9.0.6", "terser": "^5.26.0" diff --git a/packages/rulesets/scripts/compile-schemas.ts b/packages/rulesets/scripts/compile-schemas.ts index 5c4169e57..7378ed7c7 100644 --- a/packages/rulesets/scripts/compile-schemas.ts +++ b/packages/rulesets/scripts/compile-schemas.ts @@ -13,7 +13,9 @@ import { sync } from 'gzip-size'; const cwd = path.join(__dirname, '../src'); const schemas = [ - 'oas/schemas/json-schema-draft-04.json', + 'oas/schemas/json-schema/draft-04.json', + 'oas/schemas/json-schema/draft-2020-12/index.json', + 'oas/schemas/json-schema/draft-2020-12/validation.json', 'oas/schemas/oas/v2.0.json', 'oas/schemas/oas/v3.0.json', 'oas/schemas/oas/v3.1/dialect.schema.json', diff --git a/packages/rulesets/src/oas/__tests__/oas3-schema.test.ts b/packages/rulesets/src/oas/__tests__/oas3-schema.test.ts index cfbbe68dc..67a2024be 100644 --- a/packages/rulesets/src/oas/__tests__/oas3-schema.test.ts +++ b/packages/rulesets/src/oas/__tests__/oas3-schema.test.ts @@ -150,52 +150,6 @@ testRule('oas3-schema', [ ], }, - { - name: 'oas3.1: paths is not required', - document: { - openapi: '3.1.0', - info: { - title: 'Example jsonSchemaDialect error', - version: '1.0.0', - }, - webhooks: {}, - }, - errors: [], - }, - - { - name: 'oas3.1: uri template as server url', - document: { - openapi: '3.1.0', - info: { - title: 'Server URL may have variables', - version: '1.0.0', - }, - webhooks: {}, - // https://spec.openapis.org/oas/v3.1.0#server-object-example - servers: [ - { - url: 'https://{username}.gigantic-server.com:{port}/{basePath}', - description: 'The production API server', - variables: { - username: { - default: 'demo', - description: 'this value is assigned by the service provider, in this example `gigantic-server.com`', - }, - port: { - enum: ['8443', '443'], - default: '8443', - }, - basePath: { - default: 'v2', - }, - }, - }, - ], - }, - errors: [], - }, - { name: 'oas3.0: validate parameters', document: { @@ -319,6 +273,7 @@ testRule('oas3-schema', [ }, ], }, + { name: 'oas3.0: validate responses', document: { @@ -383,4 +338,246 @@ testRule('oas3-schema', [ }, ], }, + + { + name: 'oas3.0: validate schemas', + document: { + openapi: '3.0.3', + info: { + title: 'our-api', + version: '1.0', + }, + paths: { + '/config': { + parameters: [ + { + schema: null, + name: 'id', + in: 'query', + required: false, + description: 'Id of an existing config.', + }, + ], + get: { + summary: 'Get User Info by User ID', + operationId: 'get-users-settings', + responses: { + '200': { + description: 'Settings for User Found', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + key: { + type: 'string,', + }, + value: { + type: 'string', + }, + }, + required: ['key', 'value'], + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Schema: { + type: 'object', + additionalProperties: { + type: 'int', + }, + }, + Schema_2: { + type: 'object', + additionalProperties: 'invalid', + }, + }, + }, + }, + errors: [ + { + message: '"schema" property must be object.', + path: ['paths', '/config', 'parameters', '0', 'schema'], + severity: DiagnosticSeverity.Error, + }, + { + message: + '"type" property must be equal to one of the allowed values: "array", "boolean", "integer", "number", "object", "string". Did you mean "string"?.', + path: [ + 'paths', + '/config', + 'get', + 'responses', + '200', + 'content', + 'application/json', + 'schema', + 'properties', + 'key', + 'type', + ], + }, + { + message: + '"type" property must be equal to one of the allowed values: "array", "boolean", "integer", "number", "object", "string". Did you mean "integer"?.', + path: ['components', 'schemas', 'Schema', 'additionalProperties', 'type'], + severity: DiagnosticSeverity.Error, + }, + { + message: '"additionalProperties" property must be a valid Schema Object.', + path: ['components', 'schemas', 'Schema_2', 'additionalProperties'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'oas3.1: paths is not required', + document: { + openapi: '3.1.0', + info: { + title: 'Example jsonSchemaDialect error', + version: '1.0.0', + }, + webhooks: {}, + }, + errors: [], + }, + + { + name: 'oas3.1: validate schemas', + document: { + openapi: '3.1.0', + info: { + title: 'our-api', + version: '1.0', + }, + paths: { + '/config': { + parameters: [ + { + schema: null, + name: 'id', + in: 'query', + required: false, + description: 'Id of an existing config.', + }, + ], + get: { + summary: 'Get User Info by User ID', + operationId: 'get-users-settings', + responses: { + '200': { + description: 'Settings for User Found', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + key: { + type: 'string,', + }, + value: { + type: 'string', + }, + }, + required: ['key', 'value'], + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Schema: { + type: 'object', + additionalProperties: { + type: 'int', + }, + }, + Schema_2: { + type: 'object', + additionalProperties: 'invalid', + }, + }, + }, + }, + errors: [ + { + message: '"schema" property must be a valid Schema Object.', + path: ['paths', '/config', 'parameters', '0', 'schema'], + severity: DiagnosticSeverity.Error, + }, + { + message: + '"type" property must be equal to one of the allowed values: "array", "boolean", "integer", "null", "number", "object", "string". Did you mean "string"?.', + path: [ + 'paths', + '/config', + 'get', + 'responses', + '200', + 'content', + 'application/json', + 'schema', + 'properties', + 'key', + 'type', + ], + }, + { + message: + '"type" property must be equal to one of the allowed values: "array", "boolean", "integer", "null", "number", "object", "string". Did you mean "integer"?.', + path: ['components', 'schemas', 'Schema', 'additionalProperties', 'type'], + severity: DiagnosticSeverity.Error, + }, + { + message: '"additionalProperties" property must be a valid Schema Object.', + path: ['components', 'schemas', 'Schema_2', 'additionalProperties'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'oas3.1: uri template as server url', + document: { + openapi: '3.1.0', + info: { + title: 'Server URL may have variables', + version: '1.0.0', + }, + webhooks: {}, + // https://spec.openapis.org/oas/v3.1.0#server-object-example + servers: [ + { + url: 'https://{username}.gigantic-server.com:{port}/{basePath}', + description: 'The production API server', + variables: { + username: { + default: 'demo', + description: 'this value is assigned by the service provider, in this example `gigantic-server.com`', + }, + port: { + enum: ['8443', '443'], + default: '8443', + }, + basePath: { + default: 'v2', + }, + }, + }, + ], + }, + errors: [], + }, ]); diff --git a/packages/rulesets/src/oas/functions/oasDocumentSchema.ts b/packages/rulesets/src/oas/functions/oasDocumentSchema.ts index cf349ad39..15cda8a4d 100644 --- a/packages/rulesets/src/oas/functions/oasDocumentSchema.ts +++ b/packages/rulesets/src/oas/functions/oasDocumentSchema.ts @@ -16,17 +16,14 @@ export default createRulesetFunction( const formats = context.document.formats; if (formats === null || formats === void 0) return; - const validator = formats.has(oas2) - ? validators.oas2_0 - : formats.has(oas3_1) - ? validators.oas3_1 - : validators.oas3_0; + const schema = formats.has(oas2) ? 'oas2_0' : formats.has(oas3_1) ? 'oas3_1' : 'oas3_0'; + const validator = validators[schema]; validator(input); - const errors = validator['errors'] as ErrorObject[] | undefined; + const errors = validator['errors'] as ErrorObject[] | null; - return errors?.filter(isRelevantError).map(e => processError(input, e)); + return errors?.reduce((errors, e) => processError(errors, input, schema, e), []); }, ); @@ -34,19 +31,27 @@ function isRelevantError(error: ErrorObject): boolean { return error.keyword !== 'if'; } -function processError(input: unknown, error: ErrorObject): IFunctionResult { +function processError( + errors: IFunctionResult[], + input: unknown, + schema: 'oas2_0' | 'oas3_0' | 'oas3_1', + error: ErrorObject, +): IFunctionResult[] { + if (!isRelevantError(error)) { + return errors; + } + const path = error.instancePath === '' ? [] : error.instancePath.slice(1).split('/'); const property = path.length === 0 ? null : path[path.length - 1]; + let message: string; + switch (error.keyword) { case 'additionalProperties': { const additionalProperty = error.params['additionalProperty'] as string; path.push(additionalProperty); - - return { - message: `Property "${additionalProperty}" is not expected to be here`, - path, - }; + message = `Property "${additionalProperty}" is not expected to be here`; + break; } case 'enum': { @@ -71,24 +76,24 @@ function processError(input: unknown, error: ErrorObject): IFunctionResult { } } - return { - message: `${cleanAjvMessage(property, error.message)}: ${printedValues}${suggestion}`, - path, - }; + message = `${cleanAjvMessage(property, error.message)}: ${printedValues}${suggestion}`; + break; } case 'errorMessage': - return { - message: String(error.message), - path, - }; + message = String(error.message); + break; default: - return { - message: cleanAjvMessage(property, error.message), - path, - }; + message = cleanAjvMessage(property, error.message); } + + errors.push({ + message, + path, + }); + + return errors; } function findBestMatch(value: string, allowedValues: unknown[]): string | null { diff --git a/packages/rulesets/src/oas/schemas/json-schema/LICENSE b/packages/rulesets/src/oas/schemas/json-schema/LICENSE new file mode 100644 index 000000000..397909a84 --- /dev/null +++ b/packages/rulesets/src/oas/schemas/json-schema/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2021 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/rulesets/src/oas/schemas/json-schema/README.md b/packages/rulesets/src/oas/schemas/json-schema/README.md new file mode 100644 index 000000000..9e0198fdf --- /dev/null +++ b/packages/rulesets/src/oas/schemas/json-schema/README.md @@ -0,0 +1,2 @@ +The schemas here are based on https://github.com/ajv-validator/ajv with one change related to the validation of the "type" property to yield more useful validation results. +draft-04 is based on https://github.com/ajv-validator/ajv-draft-04 diff --git a/packages/rulesets/src/oas/schemas/json-schema-draft-04.json b/packages/rulesets/src/oas/schemas/json-schema/draft-04.json similarity index 100% rename from packages/rulesets/src/oas/schemas/json-schema-draft-04.json rename to packages/rulesets/src/oas/schemas/json-schema/draft-04.json diff --git a/packages/rulesets/src/oas/schemas/json-schema/draft-2020-12/index.json b/packages/rulesets/src/oas/schemas/json-schema/draft-2020-12/index.json new file mode 100644 index 000000000..fc4b1b5b4 --- /dev/null +++ b/packages/rulesets/src/oas/schemas/json-schema/draft-2020-12/index.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://stoplight.io/json-schema/draft/2020-12", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/content": true + }, + "$dynamicAnchor": "meta", + + "title": "Core and Validation specifications meta-schema", + + "if": { + "type": "object" + }, + "then": { + "allOf": [ + { "$ref": "https://json-schema.org/draft/2020-12/meta/core" }, + { "$ref": "https://json-schema.org/draft/2020-12/meta/applicator" }, + { "$ref": "https://json-schema.org/draft/2020-12/meta/unevaluated" }, + { "$ref": "https://stoplight.io/json-schema/draft/2020-12/meta/validation" }, + { "$ref": "https://json-schema.org/draft/2020-12/meta/meta-data" }, + { "$ref": "https://json-schema.org/draft/2020-12/meta/format-annotation" }, + { "$ref": "https://json-schema.org/draft/2020-12/meta/content" } + ], + "properties": { + "definitions": { + "$ref": "https://json-schema.org/draft/2020-12/schema#/properties/definitions" + }, + "dependencies": { + "$ref": "https://json-schema.org/draft/2020-12/schema#/properties/dependencies" + }, + "$recursiveAnchor": { + "$ref": "https://json-schema.org/draft/2020-12/schema#/properties/%24recursiveAnchor" + }, + "$recursiveRef": { + "$ref": "https://json-schema.org/draft/2020-12/schema#/properties/%24recursiveRef" + } + } + }, + "else": { + "if": { + "type": "boolean" + }, + "then": true, + "else": { + "not": true, + "errorMessage": "\"{{property}}\" property must be a valid Schema Object" + } + }, + "$comment": "This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use." +} diff --git a/packages/rulesets/src/oas/schemas/json-schema/draft-2020-12/validation.json b/packages/rulesets/src/oas/schemas/json-schema/draft-2020-12/validation.json new file mode 100644 index 000000000..29c4e927e --- /dev/null +++ b/packages/rulesets/src/oas/schemas/json-schema/draft-2020-12/validation.json @@ -0,0 +1,102 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://stoplight.io/json-schema/draft/2020-12/meta/validation", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/validation": true + }, + "$dynamicAnchor": "meta", + + "title": "Validation vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "type": { + "if": { + "type": "string" + }, + "then": { + "$ref": "#/$defs/simpleTypes" + }, + "else": { + "if": { + "type": "array" + }, + "then": { + "type": "array", + "items": { "$ref": "#/$defs/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + }, + "else": { + "not": true, + "errorMessage": "\"type\" property must be either a string or an array of strings" + } + } + }, + "const": true, + "enum": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/$defs/nonNegativeInteger" }, + "minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "maxItems": { "$ref": "#/$defs/nonNegativeInteger" }, + "minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxContains": { "$ref": "#/$defs/nonNegativeInteger" }, + "minContains": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 1 + }, + "maxProperties": { "$ref": "#/$defs/nonNegativeInteger" }, + "minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/$defs/stringArray" }, + "dependentRequired": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/stringArray" + } + } + }, + "$defs": { + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 0 + }, + "simpleTypes": { + "enum": ["array", "boolean", "integer", "null", "number", "object", "string"] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + } +} diff --git a/packages/rulesets/src/oas/schemas/oas/v2.0.json b/packages/rulesets/src/oas/schemas/oas/v2.0.json index 3c0c73047..4eee714a3 100644 --- a/packages/rulesets/src/oas/schemas/oas/v2.0.json +++ b/packages/rulesets/src/oas/schemas/oas/v2.0.json @@ -350,14 +350,16 @@ } }, "responseValue": { - "oneOf": [ - { - "$ref": "#/definitions/response" - }, - { - "$ref": "#/definitions/jsonReference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/jsonReference" + }, + "else": { + "$ref": "#/definitions/response" + } }, "response": { "type": "object", @@ -930,14 +932,22 @@ "$ref": "http://json-schema.org/draft-04/schema#/properties/enum" }, "additionalProperties": { - "anyOf": [ - { - "$ref": "#/definitions/schema" - }, - { + "if": { + "type": "object" + }, + "then": { + "$ref": "#/definitions/schema" + }, + "else": { + "if": { "type": "boolean" + }, + "then": true, + "else": { + "not": true, + "errorMessage": "\"additionalProperties\" property must be a valid schema" } - ], + }, "default": {} }, "type": { @@ -1379,14 +1389,16 @@ "description": "The parameters needed to send a valid API call.", "additionalItems": false, "items": { - "oneOf": [ - { - "$ref": "#/definitions/parameter" - }, - { - "$ref": "#/definitions/jsonReference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/jsonReference" + }, + "else": { + "$ref": "#/definitions/parameter" + } }, "uniqueItems": true }, diff --git a/packages/rulesets/src/oas/schemas/oas/v3.0.json b/packages/rulesets/src/oas/schemas/oas/v3.0.json index 867ee6e48..ff86a3c8f 100644 --- a/packages/rulesets/src/oas/schemas/oas/v3.0.json +++ b/packages/rulesets/src/oas/schemas/oas/v3.0.json @@ -497,14 +497,22 @@ "$ref": "#/definitions/Reference" }, "else": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { + "if": { + "type": "object" + }, + "then": { + "$ref": "#/definitions/Schema" + }, + "else": { + "if": { "type": "boolean" + }, + "then": true, + "else": { + "not": true, + "errorMessage": "\"additionalProperties\" property must be a valid Schema Object" } - ] + } }, "default": true }, diff --git a/packages/rulesets/src/oas/schemas/oas/v3.1/dialect.schema.json b/packages/rulesets/src/oas/schemas/oas/v3.1/dialect.schema.json index 3b2064572..53a56900a 100644 --- a/packages/rulesets/src/oas/schemas/oas/v3.1/dialect.schema.json +++ b/packages/rulesets/src/oas/schemas/oas/v3.1/dialect.schema.json @@ -16,10 +16,23 @@ "https://spec.openapis.org/oas/3.1/vocab/base": false }, - "$dynamicAnchor": "meta", - - "allOf": [ - { "$ref": "https://json-schema.org/draft/2020-12/schema" }, - { "$ref": "https://spec.openapis.org/oas/3.1/meta/base" } - ] + "if": { + "type": "object" + }, + "then": { + "allOf": [ + { "$ref": "https://stoplight.io/json-schema/draft/2020-12" }, + { "$ref": "https://spec.openapis.org/oas/3.1/meta/base" } + ] + }, + "else": { + "if": { + "type": "boolean" + }, + "then": true, + "else": { + "not": true, + "errorMessage": "\"{{property}}\" property must be a valid Schema Object" + } + } } diff --git a/packages/rulesets/src/oas/schemas/oas/v3.1/index.json b/packages/rulesets/src/oas/schemas/oas/v3.1/index.json index 51b7c14de..128e844fd 100644 --- a/packages/rulesets/src/oas/schemas/oas/v3.1/index.json +++ b/packages/rulesets/src/oas/schemas/oas/v3.1/index.json @@ -967,8 +967,7 @@ }, "schema": { "$comment": "https://spec.openapis.org/oas/v3.1.0#schema-object", - "$dynamicAnchor": "meta", - "type": ["object", "boolean"] + "$ref": "https://spec.openapis.org/oas/3.1/dialect/base" }, "security-scheme": { "$comment": "https://spec.openapis.org/oas/v3.1.0#security-scheme-object", diff --git a/packages/rulesets/src/oas/schemas/oas/v3.1/meta.schema.json b/packages/rulesets/src/oas/schemas/oas/v3.1/meta.schema.json index e8a20ef48..ca4f2b03f 100644 --- a/packages/rulesets/src/oas/schemas/oas/v3.1/meta.schema.json +++ b/packages/rulesets/src/oas/schemas/oas/v3.1/meta.schema.json @@ -9,16 +9,28 @@ "https://spec.openapis.org/oas/3.1/vocab/base": true }, - "$dynamicAnchor": "meta", - - "type": ["object", "boolean"], - "properties": { - "example": true, - "discriminator": { "$ref": "#/$defs/discriminator" }, - "externalDocs": { "$ref": "#/$defs/external-docs" }, - "xml": { "$ref": "#/$defs/xml" } + "if": { + "type": "object" + }, + "then": { + "type": "object", + "properties": { + "example": true, + "discriminator": { "$ref": "#/$defs/discriminator" }, + "externalDocs": { "$ref": "#/$defs/external-docs" }, + "xml": { "$ref": "#/$defs/xml" } + } + }, + "else": { + "if": { + "type": "boolean" + }, + "then": true, + "else": { + "not": true, + "errorMessage": "\"{{property}}\" property must be a valid Schema Object" + } }, - "$defs": { "extensible": { "patternProperties": { diff --git a/yarn.lock b/yarn.lock index f2fb6dfc8..387160e49 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2850,6 +2850,7 @@ __metadata: "@types/json-schema": ^7.0.7 ajv: ^8.12.0 ajv-formats: ~2.1.0 + ajv-merge-patch: ^5.0.1 gzip-size: ^6.0.0 immer: ^9.0.6 json-schema-traverse: ^1.0.0 @@ -3723,6 +3724,18 @@ __metadata: languageName: node linkType: hard +"ajv-merge-patch@npm:^5.0.1": + version: 5.0.1 + resolution: "ajv-merge-patch@npm:5.0.1" + dependencies: + fast-json-patch: ^2.0.6 + json-merge-patch: ^1.0.2 + peerDependencies: + ajv: ">=8.0.0" + checksum: 1f9354c26fe7af840d50fbd83b29ab65f3edeeb9f75c1257a42a89ee67526b49fa3232e5989ceb30090e38caf6e437d65623e5cef0a538a36e3d872c03e0ec4e + languageName: node + linkType: hard + "ajv@npm:^6.10.0, ajv@npm:^6.12.4": version: 6.12.6 resolution: "ajv@npm:6.12.6" @@ -6293,6 +6306,13 @@ __metadata: languageName: node linkType: hard +"fast-deep-equal@npm:^2.0.1": + version: 2.0.1 + resolution: "fast-deep-equal@npm:2.0.1" + checksum: b701835a87985e0ec4925bdf1f0c1e7eb56309b5d12d534d5b4b69d95a54d65bb16861c081781ead55f73f12d6c60ba668713391ee7fbf6b0567026f579b7b0b + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -6320,6 +6340,15 @@ __metadata: languageName: node linkType: hard +"fast-json-patch@npm:^2.0.6": + version: 2.2.1 + resolution: "fast-json-patch@npm:2.2.1" + dependencies: + fast-deep-equal: ^2.0.1 + checksum: 955aebb3f873d1fb0452a5d8c34865ce4c3c6cdafeb7d3ad98d43b467de9a5a0d304132f8595fd2b373f8f4d200605947e865286b180f3a55e8377a634893164 + languageName: node + linkType: hard + "fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.0.0": version: 2.0.0 resolution: "fast-json-stable-stringify@npm:2.0.0" @@ -8415,6 +8444,15 @@ __metadata: languageName: node linkType: hard +"json-merge-patch@npm:^1.0.2": + version: 1.0.2 + resolution: "json-merge-patch@npm:1.0.2" + dependencies: + fast-deep-equal: ^3.1.3 + checksum: 06867dbb93c9c3a698fba8a89f5ec1bd7a19697667a97d084d893c3ecd9ccecac07f251a531ffdf0c80df042ef3f33b5f67cdd7d73933c9cf7d9ebeaf1be24f5 + languageName: node + linkType: hard + "json-parse-better-errors@npm:^1.0.1": version: 1.0.2 resolution: "json-parse-better-errors@npm:1.0.2" From 33a4564f0a50c17097a9edefb992b8e956d2d4ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Fri, 12 Jan 2024 17:13:50 +0100 Subject: [PATCH 5/6] docs(repo): mention oas3-schema --- docs/reference/openapi-rules.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/reference/openapi-rules.md b/docs/reference/openapi-rules.md index d55e43772..584a8261c 100644 --- a/docs/reference/openapi-rules.md +++ b/docs/reference/openapi-rules.md @@ -667,6 +667,8 @@ Parameter objects should have a `description`. ### oas3-schema Validate structure of OpenAPI v3 specification. +If OpenAPI 3.1.0 is used, `jsonSchemaDialect` is not respected and the draft 2020-12 is applied. +If you define your own `jsonSchemaDialect`, you'll most likely want to disable this rule. **Recommended:** Yes From 28f055c1e3414ad80d9068939f6814ed16760123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Tue, 16 Jan 2024 15:19:44 +0100 Subject: [PATCH 6/6] chore(rulesets): remove ajv-merge-patch dep --- packages/rulesets/package.json | 20 ++---------------- yarn.lock | 38 ---------------------------------- 2 files changed, 2 insertions(+), 56 deletions(-) diff --git a/packages/rulesets/package.json b/packages/rulesets/package.json index 47ea02b23..3c157585e 100644 --- a/packages/rulesets/package.json +++ b/packages/rulesets/package.json @@ -8,26 +8,11 @@ "node": ">=12" }, "license": "Apache-2.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", "files": [ "/dist" ], - "type": "commonjs", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - }, - "./oas": { - "types": "./dist/oas/index.d.ts", - "default": "./dist/oas/index.js" - }, - "./asyncapi": { - "types": "./dist/asyncapi/index.d.ts", - "default": "./dist/asyncapi/index.js" - } - }, "repository": { "type": "git", "url": "https://github.com/stoplightio/spectral.git" @@ -53,7 +38,6 @@ "@stoplight/path": "^1.3.2", "@stoplight/spectral-parsers": "*", "@stoplight/spectral-ref-resolver": "*", - "ajv-merge-patch": "^5.0.1", "gzip-size": "^6.0.0", "immer": "^9.0.6", "terser": "^5.26.0" diff --git a/yarn.lock b/yarn.lock index 387160e49..f2fb6dfc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2850,7 +2850,6 @@ __metadata: "@types/json-schema": ^7.0.7 ajv: ^8.12.0 ajv-formats: ~2.1.0 - ajv-merge-patch: ^5.0.1 gzip-size: ^6.0.0 immer: ^9.0.6 json-schema-traverse: ^1.0.0 @@ -3724,18 +3723,6 @@ __metadata: languageName: node linkType: hard -"ajv-merge-patch@npm:^5.0.1": - version: 5.0.1 - resolution: "ajv-merge-patch@npm:5.0.1" - dependencies: - fast-json-patch: ^2.0.6 - json-merge-patch: ^1.0.2 - peerDependencies: - ajv: ">=8.0.0" - checksum: 1f9354c26fe7af840d50fbd83b29ab65f3edeeb9f75c1257a42a89ee67526b49fa3232e5989ceb30090e38caf6e437d65623e5cef0a538a36e3d872c03e0ec4e - languageName: node - linkType: hard - "ajv@npm:^6.10.0, ajv@npm:^6.12.4": version: 6.12.6 resolution: "ajv@npm:6.12.6" @@ -6306,13 +6293,6 @@ __metadata: languageName: node linkType: hard -"fast-deep-equal@npm:^2.0.1": - version: 2.0.1 - resolution: "fast-deep-equal@npm:2.0.1" - checksum: b701835a87985e0ec4925bdf1f0c1e7eb56309b5d12d534d5b4b69d95a54d65bb16861c081781ead55f73f12d6c60ba668713391ee7fbf6b0567026f579b7b0b - languageName: node - linkType: hard - "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -6340,15 +6320,6 @@ __metadata: languageName: node linkType: hard -"fast-json-patch@npm:^2.0.6": - version: 2.2.1 - resolution: "fast-json-patch@npm:2.2.1" - dependencies: - fast-deep-equal: ^2.0.1 - checksum: 955aebb3f873d1fb0452a5d8c34865ce4c3c6cdafeb7d3ad98d43b467de9a5a0d304132f8595fd2b373f8f4d200605947e865286b180f3a55e8377a634893164 - languageName: node - linkType: hard - "fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.0.0": version: 2.0.0 resolution: "fast-json-stable-stringify@npm:2.0.0" @@ -8444,15 +8415,6 @@ __metadata: languageName: node linkType: hard -"json-merge-patch@npm:^1.0.2": - version: 1.0.2 - resolution: "json-merge-patch@npm:1.0.2" - dependencies: - fast-deep-equal: ^3.1.3 - checksum: 06867dbb93c9c3a698fba8a89f5ec1bd7a19697667a97d084d893c3ecd9ccecac07f251a531ffdf0c80df042ef3f33b5f67cdd7d73933c9cf7d9ebeaf1be24f5 - languageName: node - linkType: hard - "json-parse-better-errors@npm:^1.0.1": version: 1.0.2 resolution: "json-parse-better-errors@npm:1.0.2"