From 8df2c36d7461a86b3f6fb77fcd1759ed0c3750a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Fri, 3 May 2024 16:52:27 +0200 Subject: [PATCH] feat(rulesets): improve {oas2,oas3}-valid-schema rule (#2574) * feat(rulesets): improve {oas2,oas3}-valid-schema rule * build(rulesets): try sth * build(rulesets): abandon props mangling * feat(rulesets): improve oas3.1 schema * docs(repo): mention oas3-schema * chore(rulesets): remove ajv-merge-patch dep --------- Co-authored-by: Nauman --- .eslintignore | 2 +- .gitignore | 1 + docs/reference/openapi-rules.md | 2 + package.json | 7 +- packages/rulesets/package.json | 13 +- packages/rulesets/scripts/compile-schemas.ts | 89 ++ .../src/oas/__tests__/oas2-schema.test.ts | 32 + .../src/oas/__tests__/oas3-schema.test.ts | 501 +++++++++-- .../__tests__/oasDocumentSchema.test.ts | 322 ------- .../oas/functions/__tests__/oasSchema.test.ts | 47 +- .../src/oas/functions/oasDocumentSchema.ts | 182 ++-- packages/rulesets/src/oas/index.ts | 2 + .../src/oas/schemas/__fixtures__/validate.ts | 0 .../src/oas/schemas/json-schema/LICENSE | 21 + .../src/oas/schemas/json-schema/README.md | 2 + .../src/oas/schemas/json-schema/draft-04.json | 137 +++ .../json-schema/draft-2020-12/index.json | 56 ++ .../json-schema/draft-2020-12/validation.json | 102 +++ packages/rulesets/src/oas/schemas/oas/LICENSE | 201 +++++ .../rulesets/src/oas/schemas/oas/README.md | 1 + .../oas/schemas/{2.0.json => oas/v2.0.json} | 82 +- .../oas/schemas/{3.0.json => oas/v3.0.json} | 789 +++++++++++------- .../oas/schemas/oas/v3.1/dialect.schema.json | 38 + .../schemas/{3.1.json => oas/v3.1/index.json} | 277 +++--- .../src/oas/schemas/oas/v3.1/meta.schema.json | 99 +++ test-harness/scenarios/oas3-schema.scenario | 21 +- .../scenarios/oas3.1/petstore.scenario | 3 +- yarn.lock | 86 +- 28 files changed, 2126 insertions(+), 989 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/schemas/__fixtures__/validate.ts create mode 100644 packages/rulesets/src/oas/schemas/json-schema/LICENSE create mode 100644 packages/rulesets/src/oas/schemas/json-schema/README.md create mode 100644 packages/rulesets/src/oas/schemas/json-schema/draft-04.json 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 create mode 100644 packages/rulesets/src/oas/schemas/oas/LICENSE create mode 100644 packages/rulesets/src/oas/schemas/oas/README.md rename packages/rulesets/src/oas/schemas/{2.0.json => oas/v2.0.json} (97%) rename packages/rulesets/src/oas/schemas/{3.0.json => oas/v3.0.json} (72%) 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..5c2348a5f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,6 @@ **/__fixtures__/** -/test-harness/**/*.yaml /test-harness/tests/ /packages/*/dist +/packages/rulesets/src/oas/schemas/validators.ts /packages/*/CHANGELOG.md packages/formatters/src/html/templates.ts diff --git a/.gitignore b/.gitignore index c8bc0c569..0ad7557a3 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules !.yarn/versions packages/formatters/src/html/templates.ts +packages/rulesets/src/oas/schemas/validators.ts packages/cli/binaries packages/*/src/version.ts /test-harness/tmp/ diff --git a/docs/reference/openapi-rules.md b/docs/reference/openapi-rules.md index 56b06601d..e38848806 100644 --- a/docs/reference/openapi-rules.md +++ b/docs/reference/openapi-rules.md @@ -665,6 +665,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 diff --git a/package.json b/package.json index ec485aa4b..d99759294 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", @@ -31,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", @@ -137,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 7ddcfe8d6..3c157585e 100644 --- a/packages/rulesets/package.json +++ b/packages/rulesets/package.json @@ -27,9 +27,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 +38,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..7378ed7c7 --- /dev/null +++ b/packages/rulesets/scripts/compile-schemas.ts @@ -0,0 +1,89 @@ +/* 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/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', + '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, + }, + }); + + ajvFormats(ajv); + ajvErrors(ajv); + + 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', + 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, + module: true, + format: { + comments: false, + }, + }) + ).code as string; + + 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..67a2024be 100644 --- a/packages/rulesets/src/oas/__tests__/oas3-schema.test.ts +++ b/packages/rulesets/src/oas/__tests__/oas3-schema.test.ts @@ -4,14 +4,101 @@ import testRule from '../../__tests__/__helpers__/tester'; testRule('oas3-schema', [ { name: 'human-readable Ajv errors', - document: require('./__fixtures__/petstore.invalid-schema.oas3.json'), + document: { + openapi: '3.0.0', + info: { + version: '1.0.0', + title: 'Swagger Petstore', + license: { + name: 'MIT', + }, + contact: { + email: 'bar@foo', + }, + description: 'test', + }, + servers: [ + { + url: 'http://petstore.swagger.io/v1', + }, + ], + paths: { + '/pets': { + get: { + summary: 'List all pets', + operationId: 'listPets', + description: 'test', + tags: ['pets'], + parameters: [ + { + name: 'limit', + in: 'query', + description: 'How many items to return at one time (max 100)', + required: false, + schema: { + type: 'integer', + format: 'int32', + }, + }, + ], + responses: { + '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: '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: '"email" property must match format "email".', path: ['info', 'contact', 'email'], }, { - message: '"header-1" property must have required property "schema".', + message: '"schema" or "content" must be present.', path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'], }, { @@ -26,41 +113,184 @@ testRule('oas3-schema', [ }, { - name: 'sibling additionalProperties errors', + name: 'oas3.1: jsonSchemaDialect', document: { - openapi: '3.0.0', + openapi: '3.1.0', + info: { + title: 'Example jsonSchemaDialect error', + version: '1.0.0', + }, + paths: {}, + jsonSchemaDialect: null, + }, + errors: [ + { + message: '"jsonSchemaDialect" property must be string.', + path: ['jsonSchemaDialect'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'oas3.1: missing webhooks/components/paths', + document: { + openapi: '3.1.0', info: { - title: 'Siblings', + title: 'Missing webhooks/components/paths', + version: '1.0.0', + }, + }, + errors: [ + { + message: 'The document must have either "paths", "webhooks" or "components".', + path: [], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'oas3.0: validate parameters', + document: { + openapi: '3.0.1', + info: { + title: 'response example', version: '1.0', }, - 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', - tags: ['pets'], - requestBody: { - description: 'Pet object that needs to be added to the store', - required: true, - content: { - 'application/json': { - schema: { - type: 'object', - }, + '/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', @@ -86,57 +316,122 @@ testRule('oas3-schema', [ }, }, 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', '/pets', 'post', 'responses', '42'], + path: ['paths', '/user', 'post', 'responses', '42'], severity: DiagnosticSeverity.Error, }, { message: 'Property "9999" is not expected to be here.', - path: ['paths', '/pets', 'post', 'responses', '9999'], + path: ['paths', '/user', 'post', 'responses', '9999'], severity: DiagnosticSeverity.Error, }, { message: 'Property "5xx" is not expected to be here.', - path: ['paths', '/pets', 'post', 'responses', '5xx'], + path: ['paths', '/user', 'post', 'responses', '5xx'], severity: DiagnosticSeverity.Error, }, ], }, { - name: 'oas3.1: jsonSchemaDialect', + name: 'oas3.0: validate schemas', document: { - openapi: '3.1.0', + openapi: '3.0.3', info: { - title: 'Example jsonSchemaDialect error', - version: '1.0.0', + 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', + }, + }, }, - paths: {}, - jsonSchemaDialect: null, }, errors: [ { - message: '"jsonSchemaDialect" property type must be string.', - path: ['jsonSchemaDialect'], + message: '"schema" property must be object.', + path: ['paths', '/config', 'parameters', '0', 'schema'], severity: DiagnosticSeverity.Error, }, - ], - }, - - { - name: 'oas3.1: missing webhooks/components/paths', - document: { - openapi: '3.1.0', - info: { - title: 'Missing webhooks/components/paths', - version: '1.0.0', + { + 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', + ], }, - }, - errors: [ { - message: 'The document must have either "paths", "webhooks" or "components".', - path: [], + 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, }, ], @@ -155,6 +450,104 @@ testRule('oas3-schema', [ 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: { 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 index a2e731819..15cda8a4d 100644 --- a/packages/rulesets/src/oas/functions/oasDocumentSchema.ts +++ b/packages/rulesets/src/oas/functions/oasDocumentSchema.ts @@ -1,99 +1,125 @@ -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 type { IFunctionResult } from '@stoplight/spectral-core'; import { oas2, oas3_1 } from '@stoplight/spectral-formats'; +import { isPlainObject, resolveInlineRef } from '@stoplight/json'; +import type { ErrorObject } from 'ajv'; +import leven from 'leven'; -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') - ); -} +import * as validators from '../schemas/validators'; -// 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 = [ +export default createRulesetFunction( { - path: /^components\/securitySchemes\/[^/]+$/, - message: 'Invalid security scheme', + input: null, + options: null, }, -]; - -// 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'])}`; - } + function oasDocumentSchema(input, _opts, context) { + const formats = context.document.formats; + if (formats === null || formats === void 0) return; + + 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[] | null; + + return errors?.reduce((errors, e) => processError(errors, input, schema, e), []); + }, +); + +function isRelevantError(error: ErrorObject): boolean { + return error.keyword !== 'if'; +} + +function processError( + errors: IFunctionResult[], + input: unknown, + schema: 'oas2_0' | 'oas3_0' | 'oas3_1', + error: ErrorObject, +): IFunctionResult[] { + if (!isRelevantError(error)) { + return errors; } - for (let i = 0; i < errors.length; i++) { - const error = errors[i]; + const path = error.instancePath === '' ? [] : error.instancePath.slice(1).split('/'); + const property = path.length === 0 ? null : path[path.length - 1]; + + let message: string; - 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--; + switch (error.keyword) { + case 'additionalProperties': { + const additionalProperty = error.params['additionalProperty'] as string; + path.push(additionalProperty); + message = `Property "${additionalProperty}" is not expected to be here`; + break; } - } -} -function applyManualReplacements(errors: IFunctionResult[]): void { - for (const error of errors) { - if (error.path === void 0) continue; + case 'enum': { + const allowedValues = error.params['allowedValues'] as unknown[]; + const printedValues = allowedValues.map(value => JSON.stringify(value)).join(', '); + let suggestion: string; - const joinedPath = error.path.join('/'); + if (!isPlainObject(input)) { + suggestion = ''; + } else { + const value = resolveInlineRef(input, `#${error.instancePath}`); + if (typeof value !== 'string') { + suggestion = ''; + } else { + const bestMatch = findBestMatch(value, allowedValues); - for (const mappedError of ERROR_MAP) { - if (mappedError.path.test(joinedPath)) { - error.message = mappedError.message; - break; + if (bestMatch !== null) { + suggestion = `. Did you mean "${bestMatch}"?`; + } else { + suggestion = ''; + } + } } + + message = `${cleanAjvMessage(property, error.message)}: ${printedValues}${suggestion}`; + break; } + + case 'errorMessage': + message = String(error.message); + break; + + default: + message = cleanAjvMessage(property, error.message); } + + errors.push({ + message, + path, + }); + + return errors; } -export default createRulesetFunction( - { - input: null, - options: null, - }, - function oasDocumentSchema(targetVal, opts, context) { - const formats = context.document.formats; - if (formats === null || formats === void 0) return; +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)); - const schema = formats.has(oas2) - ? OAS_SCHEMAS['2.0'] - : formats.has(oas3_1) - ? OAS_SCHEMAS['3.1'] - : OAS_SCHEMAS['3.0']; + if (matches.length === 0) { + return null; + } - const errors = schemaFn(targetVal, { allErrors: true, schema, prepareResults }, context); + const bestMatch = matches[0]; - if (Array.isArray(errors)) { - applyManualReplacements(errors); - } + return allowedValues.length === 1 || bestMatch.weight < bestMatch.value.length ? bestMatch.value : null; +} - return errors; - }, -); +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/index.ts b/packages/rulesets/src/oas/index.ts index 67af689d7..253673754 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/__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/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 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/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/LICENSE b/packages/rulesets/src/oas/schemas/oas/LICENSE new file mode 100644 index 000000000..23b34fdff --- /dev/null +++ b/packages/rulesets/src/oas/schemas/oas/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/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 97% rename from packages/rulesets/src/oas/schemas/2.0.json rename to packages/rulesets/src/oas/schemas/oas/v2.0.json index 7fa341242..4eee714a3 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, @@ -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": { @@ -1029,7 +1039,6 @@ "$ref": "http://json-schema.org/draft-04/schema#/definitions/stringArray" }, "type": { - "type": "string", "enum": ["file"] }, "readOnly": { @@ -1048,7 +1057,6 @@ "additionalProperties": false, "properties": { "type": { - "type": "string", "enum": ["string", "number", "integer", "boolean", "array"] }, "format": { @@ -1211,7 +1219,6 @@ "required": ["type"], "properties": { "type": { - "type": "string", "enum": ["basic"] }, "description": { @@ -1230,14 +1237,12 @@ "required": ["type", "name", "in"], "properties": { "type": { - "type": "string", "enum": ["apiKey"] }, "name": { "type": "string" }, "in": { - "type": "string", "enum": ["header", "query"] }, "description": { @@ -1256,11 +1261,9 @@ "required": ["type", "flow", "authorizationUrl", "scopes"], "properties": { "type": { - "type": "string", "enum": ["oauth2"] }, "flow": { - "type": "string", "enum": ["implicit"] }, "scopes": { @@ -1286,11 +1289,9 @@ "required": ["type", "flow", "tokenUrl", "scopes"], "properties": { "type": { - "type": "string", "enum": ["oauth2"] }, "flow": { - "type": "string", "enum": ["password"] }, "scopes": { @@ -1316,12 +1317,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 +1345,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" @@ -1392,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 }, @@ -1407,18 +1406,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 72% rename from packages/rulesets/src/oas/schemas/3.0.json rename to packages/rulesets/src/oas/schemas/oas/v3.0.json index 3e500737d..ff86a3c8f 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,118 @@ "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": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } } }, "additionalProperties": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "if": { + "type": "object" }, - { - "$ref": "#/definitions/Reference" + "then": { + "$ref": "#/definitions/Schema" }, - { - "type": "boolean" + "else": { + "if": { + "type": "boolean" + }, + "then": true, + "else": { + "not": true, + "errorMessage": "\"additionalProperties\" property must be a valid Schema Object" + } } - ], + }, "default": true }, "description": { @@ -564,14 +607,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 +628,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 +650,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 +733,7 @@ "default": false }, "style": { - "type": "string", - "enum": ["simple"], + "const": "simple", "default": "simple" }, "explode": { @@ -694,14 +744,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 +767,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 +824,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 +871,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 +902,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 +940,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 +1023,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 +1068,14 @@ } } ] + }, + "else": { + "not": true, + "errorMessage": { + "not": "\"schema\" or \"content\" must be present" + } } - ] + } }, "Parameter": { "type": "object", @@ -1032,14 +1112,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 +1135,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 +1152,7 @@ "^x-": {} }, "additionalProperties": false, - "required": ["name", "in"], + "required": ["name"], "allOf": [ { "$ref": "#/definitions/ExampleXORExamples" @@ -1083,60 +1167,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 +1287,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 +1392,7 @@ }, "type": { "type": "string", - "enum": ["http"] + "const": "http" } }, "patternProperties": { @@ -1228,7 +1404,7 @@ "description": "Bearer", "properties": { "scheme": { - "enum": ["bearer"] + "const": "bearer" } } }, @@ -1240,7 +1416,7 @@ "properties": { "scheme": { "not": { - "enum": ["bearer"] + "const": "bearer" } } } @@ -1252,8 +1428,7 @@ "required": ["type", "flows"], "properties": { "type": { - "type": "string", - "enum": ["oauth2"] + "const": "oauth2" }, "flows": { "$ref": "#/definitions/OAuthFlows" @@ -1272,8 +1447,7 @@ "required": ["type", "openIdConnectUrl"], "properties": { "type": { - "type": "string", - "enum": ["openIdConnect"] + "const": "openIdConnect" }, "openIdConnectUrl": { "type": "string", @@ -1462,7 +1636,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..53a56900a --- /dev/null +++ b/packages/rulesets/src/oas/schemas/oas/v3.1/dialect.schema.json @@ -0,0 +1,38 @@ +{ + "$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 + }, + + "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/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..128e844fd 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,14 @@ "description": { "type": "string" } - }, - "unevaluatedProperties": false + } }, "schema": { - "$dynamicAnchor": "meta", - "type": ["object", "boolean"] + "$comment": "https://spec.openapis.org/oas/v3.1.0#schema-object", + "$ref": "https://spec.openapis.org/oas/3.1/dialect/base" }, "security-scheme": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#security-scheme-object", "type": "object", "properties": { "type": { @@ -1105,10 +1137,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 +1156,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 +1175,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 +1194,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 +1216,7 @@ } }, "security-requirement": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#security-requirement-object", "type": "object", "additionalProperties": { "type": "array", @@ -1184,6 +1226,7 @@ } }, "specification-extensions": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#specification-extensions", "patternProperties": { "^x-": true } @@ -1204,6 +1247,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..ca4f2b03f --- /dev/null +++ b/packages/rulesets/src/oas/schemas/oas/v3.1/meta.schema.json @@ -0,0 +1,99 @@ +{ + "$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 + }, + + "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": { + "^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 941db348b..759d0b802 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" @@ -2845,11 +2855,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 @@ -3656,12 +3669,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 @@ -3751,15 +3764,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 @@ -4885,6 +4898,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" @@ -5650,6 +5670,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" @@ -7012,6 +7039,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" @@ -8787,7 +8823,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 @@ -12011,13 +12047,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 @@ -12497,6 +12533,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"