diff --git a/.eslintignore b/.eslintignore index a4962f609..f12606a84 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,6 @@ **/__fixtures__/** -/test-harness/**/*.yaml /test-harness/tests/ /packages/*/dist +/packages/rulesets/src/oas/schemas/compiled.ts /packages/*/CHANGELOG.md packages/formatters/src/html/templates.ts diff --git a/.gitignore b/.gitignore index c8bc0c569..69365a2d2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules !.yarn/versions packages/formatters/src/html/templates.ts +packages/rulesets/src/oas/schemas/compiled.ts packages/cli/binaries packages/*/src/version.ts /test-harness/tmp/ diff --git a/package.json b/package.json index ec485aa4b..e93a3febb 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "node": "^12.20 || >= 14.13" }, "scripts": { - "clean": "rimraf .cache packages/*/{dist,.cache}", + "preclean": "yarn workspaces foreach run preclean", + "clean": "yarn preclean && rimraf .cache packages/*/{dist,.cache}", "prebuild": "yarn workspaces foreach run prebuild", "build": "yarn prebuild && tsc --build ./tsconfig.build.json && yarn postbuild", "postbuild": "yarn workspaces foreach run postbuild", diff --git a/packages/rulesets/package.json b/packages/rulesets/package.json index 7ddcfe8d6..1f9b22fc7 100644 --- a/packages/rulesets/package.json +++ b/packages/rulesets/package.json @@ -8,11 +8,26 @@ "node": ">=12" }, "license": "Apache-2.0", - "main": "dist/index.js", - "types": "dist/index.d.ts", "files": [ "/dist" ], + "type": "commonjs", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./oas": { + "types": "./dist/oas/index.d.ts", + "default": "./dist/oas/index.js" + }, + "./asyncapi": { + "types": "./dist/asyncapi/index.d.ts", + "default": "./dist/asyncapi/index.js" + } + }, "repository": { "type": "git", "url": "https://github.com/stoplightio/spectral.git" @@ -27,7 +42,7 @@ "@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", "lodash": "~4.17.21", @@ -37,6 +52,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..66fc05106 --- /dev/null +++ b/packages/rulesets/scripts/compile-schemas.ts @@ -0,0 +1,93 @@ +/* eslint-disable no-console */ +import * as fs from 'fs'; +import * as path from 'path'; +import * as process from 'process'; +import Ajv2020 from 'ajv/dist/2020.js'; +import standaloneCode from 'ajv/dist/standalone/index.js'; +import ajvErrors from 'ajv-errors'; +import ajvFormats from 'ajv-formats'; +import chalk from 'chalk'; +import { minify } from 'terser'; +import { sync } from 'gzip-size'; + +const cwd = path.join(__dirname, '../src'); + +const schemas = [ + 'oas/schemas/json-schema-draft-04.json', + 'oas/schemas/v2.0.json', + 'oas/schemas/v3.0.json', + 'oas/schemas/v3.1/dialect.schema.json', + 'oas/schemas/v3.1/meta.schema.json', + 'oas/schemas/v3.1/index.json', +].map(async schema => JSON.parse(await fs.promises.readFile(path.join(cwd, schema), 'utf8'))); + +const log = process.argv.includes('--quiet') + ? (): void => { + /* no-op */ + } + : console.log.bind(console); + +Promise.all(schemas) + .then(async schemas => { + const ajv = new Ajv2020({ + schemas, + allErrors: true, + messages: true, + strict: false, + inlineRefs: false, + formats: { + 'media-range': true, + }, + code: { + esm: true, + source: true, + optimize: 1, + }, + }); + + ajvFormats(ajv); + ajvErrors(ajv); + + const target = path.join(cwd, 'oas/schemas/compiled.ts'); + const basename = path.basename(target); + const code = standaloneCode(ajv, { + oas2_0: 'http://swagger.io/v2/schema.json', + oas3_0: 'https://spec.openapis.org/oas/3.0/schema/2019-04-02', + oas3_1: 'https://spec.openapis.org/oas/3.1/schema/2021-09-28', + }); + + const minified = ( + await minify(code, { + compress: { + passes: 2, + ecma: 2020, + }, + ecma: 2020, + module: true, + mangle: { + toplevel: true, + module: true, + }, + format: { + comments: false, + }, + }) + ).code!; + + log( + 'writing %s size is %dKB (original), %dKB (minified) %dKB (minified + gzipped)', + path.join(target, '..', basename), + Math.round((code.length / 1024) * 100) / 100, + Math.round((minified.length / 1024) * 100) / 100, + Math.round((sync(minified) / 1024) * 100) / 100, + ); + + await fs.promises.writeFile(path.join(target, '..', basename), ['// @ts-nocheck', minified].join('\n')); + }) + .then(() => { + log(chalk.green('Validators generated.')); + }) + .catch(e => { + console.error(chalk.red('Error generating validators %s'), e.message); + process.exit(1); + }); diff --git a/packages/rulesets/src/oas/__tests__/oas2-schema.test.ts b/packages/rulesets/src/oas/__tests__/oas2-schema.test.ts index 97b28290e..8f6a4c796 100644 --- a/packages/rulesets/src/oas/__tests__/oas2-schema.test.ts +++ b/packages/rulesets/src/oas/__tests__/oas2-schema.test.ts @@ -25,4 +25,36 @@ testRule('oas2-schema', [ }, ], }, + + { + name: 'validate security definitions', + document: { + swagger: '2.0', + info: { + title: 'response example', + version: '1.0', + }, + paths: { + '/user': { + get: { + responses: { + 200: { + description: 'dummy description', + }, + }, + }, + }, + }, + securityDefinitions: { + basic: null, + }, + }, + errors: [ + { + message: 'Invalid basic authentication security definition.', + path: ['securityDefinitions', 'basic'], + severity: DiagnosticSeverity.Error, + }, + ], + }, ]); diff --git a/packages/rulesets/src/oas/__tests__/oas3-schema.test.ts b/packages/rulesets/src/oas/__tests__/oas3-schema.test.ts index e2cea76a5..b8e110f7b 100644 --- a/packages/rulesets/src/oas/__tests__/oas3-schema.test.ts +++ b/packages/rulesets/src/oas/__tests__/oas3-schema.test.ts @@ -4,102 +4,110 @@ import testRule from '../../__tests__/__helpers__/tester'; testRule('oas3-schema', [ { name: 'human-readable Ajv errors', - document: require('./__fixtures__/petstore.invalid-schema.oas3.json'), - errors: [ - { - message: '"email" property must match format "email".', - path: ['info', 'contact', 'email'], - }, - { - message: '"header-1" property must have required property "schema".', - path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'], - }, - { - message: 'Property "type" is not expected to be here.', - path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1', 'type'], - }, - { - message: 'Property "op" is not expected to be here.', - path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1', 'op'], - }, - ], - }, - - { - name: 'sibling additionalProperties errors', document: { openapi: '3.0.0', info: { - title: 'Siblings', - version: '1.0', + version: '1.0.0', + title: 'Swagger Petstore', + license: { + name: 'MIT', + }, + contact: { + email: 'bar@foo', + }, + description: 'test', }, servers: [ { url: 'http://petstore.swagger.io/v1', }, ], - tags: [ - { - name: 'pets', - }, - ], paths: { '/pets': { - post: { - description: 'Add a new pet to the store', - summary: 'Create pet', - operationId: 'create_pet', + get: { + summary: 'List all pets', + operationId: 'listPets', + description: 'test', tags: ['pets'], - requestBody: { - description: 'Pet object that needs to be added to the store', - required: true, - content: { - 'application/json': { - schema: { - type: 'object', - }, + parameters: [ + { + name: 'limit', + in: 'query', + description: 'How many items to return at one time (max 100)', + required: false, + schema: { + type: 'integer', + format: 'int32', }, }, - }, + ], responses: { - '204': { - description: 'Success', - }, - '400': { - description: 'Bad request', - }, - '42': { - description: 'The answer to life, the universe, and everything', - }, - '9999': { - description: 'Four digits must be better than three', - }, - '5xx': { - description: 'Sumpin bad happened', + '200': { + description: 'A paged array of pets', + headers: { + 'x-next': { + description: 'A link to the next page of responses', + schema: { + type: 'string', + }, + }, + 'header-1': { + type: 'string', + op: 'foo', + }, + }, + content: { + 'application/json': { + schema: { + $ref: './models/pet.yaml', + }, + }, + }, }, default: { - description: 'Error', + description: 'unexpected error', + content: { + 'application/json': { + schema: { + $ref: '../common/models/error.yaml', + }, + }, + }, }, }, }, }, }, + components: { + schemas: { + Pets: { + type: 'array', + items: { + $ref: './models/pet.yaml', + }, + 'x-tags': ['Pets'], + title: 'Pets', + description: 'A list of pets.', + }, + }, + }, }, errors: [ { - message: 'Property "42" is not expected to be here.', - path: ['paths', '/pets', 'post', 'responses', '42'], - severity: DiagnosticSeverity.Error, + message: '"email" property must match format "email".', + path: ['info', 'contact', 'email'], }, { - message: 'Property "9999" is not expected to be here.', - path: ['paths', '/pets', 'post', 'responses', '9999'], - severity: DiagnosticSeverity.Error, + message: '"schema" or "content" must be present.', + path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'], }, { - message: 'Property "5xx" is not expected to be here.', - path: ['paths', '/pets', 'post', 'responses', '5xx'], - severity: DiagnosticSeverity.Error, + message: 'Property "type" is not expected to be here.', + path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1', 'type'], + }, + { + message: 'Property "op" is not expected to be here.', + path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1', 'op'], }, ], }, @@ -187,4 +195,178 @@ testRule('oas3-schema', [ }, errors: [], }, + + { + name: 'oas3.0: validate parameters', + document: { + openapi: '3.0.1', + info: { + title: 'response example', + version: '1.0', + }, + paths: { + '/user': { + get: { + responses: { + 200: { + description: 'dummy description', + }, + }, + parameters: [ + { + name: 'module_id', + in: 'cookies', + required: true, + schema: { + type: ['string', 'number'], + }, + }, + { + name: 'module_id', + required: true, + schema: { + type: 'string', + }, + }, + ], + }, + }, + }, + }, + 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, + }, + ], + }, + + { + name: 'oas3.0: validate security schemes', + document: { + openapi: '3.0.1', + info: { + title: 'response example', + version: '1.0', + }, + paths: { + '/user': { + get: { + responses: { + 200: { + description: 'dummy description', + }, + }, + }, + }, + }, + components: { + securitySchemes: { + basic: { + foo: 2, + }, + http: { + type: 'https', + scheme: 'basic', + }, + apiKey: null, + }, + }, + }, + errors: [ + { + message: 'Security scheme must have a valid type.', + path: ['components', 'securitySchemes', 'basic'], + severity: DiagnosticSeverity.Error, + }, + { + message: + '"type" property must be equal to one of the allowed values: "apiKey", "http", "oauth2", "openIdConnect". Did you mean "http"?.', + path: ['components', 'securitySchemes', 'http', 'type'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Invalid security scheme.', + path: ['components', 'securitySchemes', 'apiKey'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + { + name: 'oas3.0: validate responses', + document: { + openapi: '3.0.1', + info: { + title: 'response example', + version: '1.0', + }, + paths: { + '/user': { + get: { + operationId: 'd', + responses: { + 200: {}, + }, + }, + post: { + responses: { + '204': { + description: 'Success', + }, + '400': { + description: 'Bad request', + }, + '42': { + description: 'The answer to life, the universe, and everything', + }, + '9999': { + description: 'Four digits must be better than three', + }, + '5xx': { + description: 'Sumpin bad happened', + }, + default: { + description: 'Error', + }, + }, + }, + }, + }, + }, + errors: [ + { + message: '"200" property must have required property "description".', + path: ['paths', '/user', 'get', 'responses', '200'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Property "42" is not expected to be here.', + path: ['paths', '/user', 'post', 'responses', '42'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Property "9999" is not expected to be here.', + path: ['paths', '/user', 'post', 'responses', '9999'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Property "5xx" is not expected to be here.', + path: ['paths', '/user', 'post', 'responses', '5xx'], + severity: DiagnosticSeverity.Error, + }, + ], + }, ]); diff --git a/packages/rulesets/src/oas/functions/__tests__/oasDocumentSchema.test.ts b/packages/rulesets/src/oas/functions/__tests__/oasDocumentSchema.test.ts deleted file mode 100644 index 0f9d33914..000000000 --- a/packages/rulesets/src/oas/functions/__tests__/oasDocumentSchema.test.ts +++ /dev/null @@ -1,322 +0,0 @@ -import { DiagnosticSeverity } from '@stoplight/types'; -import { Spectral } from '@stoplight/spectral-core'; -import { prepareResults } from '../oasDocumentSchema'; - -import { ErrorObject } from 'ajv'; -import { createWithRules } from '../../__tests__/__helpers__/tester'; - -describe('oasDocumentSchema', () => { - let s: Spectral; - - beforeEach(async () => { - s = createWithRules(['oas2-schema', 'oas3-schema']); - }); - - describe('given OpenAPI 2 document', () => { - test('validate security definitions', async () => { - expect( - await s.run({ - swagger: '2.0', - info: { - title: 'response example', - version: '1.0', - }, - paths: { - '/user': { - get: { - responses: { - 200: { - description: 'dummy description', - }, - }, - }, - }, - }, - securityDefinitions: { - basic: null, - }, - }), - ).toEqual([ - { - code: 'oas2-schema', - message: 'Invalid basic authentication security definition.', - path: ['securityDefinitions', 'basic'], - severity: DiagnosticSeverity.Error, - range: expect.any(Object), - }, - ]); - }); - }); - - describe('given OpenAPI 3 document', () => { - test('validate parameters', async () => { - expect( - await s.run({ - openapi: '3.0.1', - info: { - title: 'response example', - version: '1.0', - }, - paths: { - '/user': { - get: { - responses: { - 200: { - description: 'dummy description', - }, - }, - parameters: [ - { - name: 'module_id', - in: 'bar', - required: true, - schema: { - type: ['string', 'number'], - }, - }, - ], - }, - }, - }, - }), - ).toEqual([ - { - code: 'oas3-schema', - message: '"type" property type must be string.', - path: ['paths', '/user', 'get', 'parameters', '0', 'schema', 'type'], - severity: DiagnosticSeverity.Error, - range: expect.any(Object), - }, - ]); - }); - - test('validate security schemes', async () => { - expect( - await s.run({ - openapi: '3.0.1', - info: { - title: 'response example', - version: '1.0', - }, - paths: { - '/user': { - get: { - responses: { - 200: { - description: 'dummy description', - }, - }, - }, - }, - }, - components: { - securitySchemes: { - basic: { - foo: 2, - }, - }, - }, - }), - ).toEqual([ - { - code: 'oas3-schema', - message: 'Invalid security scheme.', - path: ['components', 'securitySchemes', 'basic'], - severity: DiagnosticSeverity.Error, - range: expect.any(Object), - }, - { - code: 'oas3-schema', - message: 'Property "foo" is not expected to be here.', - path: ['components', 'securitySchemes', 'basic', 'foo'], - severity: DiagnosticSeverity.Error, - range: expect.any(Object), - }, - ]); - }); - - test('validate responses', async () => { - expect( - await s.run({ - openapi: '3.0.1', - info: { - title: 'response example', - version: '1.0', - }, - paths: { - '/user': { - get: { - operationId: 'd', - responses: { - 200: {}, - }, - }, - }, - }, - }), - ).toEqual([ - { - code: 'oas3-schema', - message: '"200" property must have required property "description".', - path: ['paths', '/user', 'get', 'responses', '200'], - severity: DiagnosticSeverity.Error, - range: expect.any(Object), - }, - ]); - }); - }); - - describe('prepareResults', () => { - test('given oneOf error one of which is required $ref property missing, picks only one error', () => { - const errors: ErrorObject[] = [ - { - keyword: 'type', - instancePath: '/paths/test/post/parameters/0/schema/type', - schemaPath: '#/properties/type/type', - params: { type: 'string' }, - message: 'must be string', - }, - { - keyword: 'required', - instancePath: '/paths/test/post/parameters/0/schema', - schemaPath: '#/definitions/Reference/required', - params: { missingProperty: '$ref' }, - message: "must have required property '$ref'", - }, - { - keyword: 'oneOf', - instancePath: '/paths/test/post/parameters/0/schema', - schemaPath: '#/properties/schema/oneOf', - params: { passingSchemas: null }, - message: 'must match exactly one schema in oneOf', - }, - ]; - - prepareResults(errors); - - expect(errors).toStrictEqual([ - { - keyword: 'type', - instancePath: '/paths/test/post/parameters/0/schema/type', - schemaPath: '#/properties/type/type', - params: { type: 'string' }, - message: 'must be string', - }, - ]); - }); - - test('given oneOf error one without any $ref property missing, picks all errors', () => { - const errors: ErrorObject[] = [ - { - keyword: 'type', - instancePath: '/paths/test/post/parameters/0/schema/type', - schemaPath: '#/properties/type/type', - params: { type: 'string' }, - message: 'must be string', - }, - { - keyword: 'type', - instancePath: '/paths/test/post/parameters/1/schema/type', - schemaPath: '#/properties/type/type', - params: { type: 'string' }, - message: 'must be string', - }, - { - keyword: 'oneOf', - instancePath: '/paths/test/post/parameters/0/schema', - schemaPath: '#/properties/schema/oneOf', - params: { passingSchemas: null }, - message: 'must match exactly one schema in oneOf', - }, - ]; - - prepareResults(errors); - - expect(errors).toStrictEqual([ - { - keyword: 'type', - instancePath: '/paths/test/post/parameters/0/schema/type', - schemaPath: '#/properties/type/type', - params: { type: 'string' }, - message: 'must be string', - }, - { - instancePath: '/paths/test/post/parameters/1/schema/type', - keyword: 'type', - message: 'must be string', - params: { - type: 'string', - }, - schemaPath: '#/properties/type/type', - }, - { - instancePath: '/paths/test/post/parameters/0/schema', - keyword: 'oneOf', - message: 'must match exactly one schema in oneOf', - params: { - passingSchemas: null, - }, - schemaPath: '#/properties/schema/oneOf', - }, - ]); - }); - - test('given errors with different data paths, picks all errors', () => { - const errors: ErrorObject[] = [ - { - keyword: 'type', - instancePath: '/paths/test/post/parameters/0/schema/type', - schemaPath: '#/properties/type/type', - params: { type: 'string' }, - message: 'must be string', - }, - { - keyword: 'required', - instancePath: '/paths/foo/post/parameters/0/schema', - schemaPath: '#/definitions/Reference/required', - params: { missingProperty: '$ref' }, - message: "must have required property '$ref'", - }, - { - keyword: 'oneOf', - instancePath: '/paths/baz/post/parameters/0/schema', - schemaPath: '#/properties/schema/oneOf', - params: { passingSchemas: null }, - message: 'must match exactly one schema in oneOf', - }, - ]; - - prepareResults(errors); - - expect(errors).toStrictEqual([ - { - instancePath: '/paths/test/post/parameters/0/schema/type', - keyword: 'type', - message: 'must be string', - params: { - type: 'string', - }, - schemaPath: '#/properties/type/type', - }, - { - instancePath: '/paths/foo/post/parameters/0/schema', - keyword: 'required', - message: "must have required property '$ref'", - params: { - missingProperty: '$ref', - }, - schemaPath: '#/definitions/Reference/required', - }, - { - instancePath: '/paths/baz/post/parameters/0/schema', - keyword: 'oneOf', - message: 'must match exactly one schema in oneOf', - params: { - passingSchemas: null, - }, - schemaPath: '#/properties/schema/oneOf', - }, - ]); - }); - }); -}); diff --git a/packages/rulesets/src/oas/functions/__tests__/oasSchema.test.ts b/packages/rulesets/src/oas/functions/__tests__/oasSchema.test.ts index f9cc46926..19a86b4d2 100644 --- a/packages/rulesets/src/oas/functions/__tests__/oasSchema.test.ts +++ b/packages/rulesets/src/oas/functions/__tests__/oasSchema.test.ts @@ -1,8 +1,8 @@ +import type { RulesetFunctionContext } from '@stoplight/spectral-core'; import { oas2, oas3, oas3_0, oas3_1 } from '@stoplight/spectral-formats'; -import { DeepPartial } from '@stoplight/types'; +import type { DeepPartial } from '@stoplight/types'; + import oasSchema from '../../functions/oasSchema'; -import { createWithRules } from '../../__tests__/__helpers__/tester'; -import { RulesetFunctionContext } from '@stoplight/spectral-core/src'; function runSchema(target: unknown, schemaObj: Record, context?: DeepPartial) { return oasSchema(target, { schema: schemaObj }, { @@ -153,45 +153,4 @@ describe('oasSchema', () => { expect(runSchema(1.5, testSchema, { document })).toEqual([]); }); - - test('should remove all redundant ajv errors', async () => { - const spectral = createWithRules(['oas3-schema', 'oas3-valid-schema-example', 'oas3-valid-media-example']); - const invalidSchema = JSON.stringify(require('../../__tests__/__fixtures__/petstore.invalid-schema.oas3.json')); - - const result = await spectral.run(invalidSchema); - - expect(result).toEqual([ - expect.objectContaining({ - code: 'oas3-schema', - message: '"email" property must match format "email".', - path: ['info', 'contact', 'email'], - }), - expect.objectContaining({ - code: 'oas3-schema', - message: '"header-1" property must have required property "schema".', - path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'], - }), - expect.objectContaining({ - code: 'oas3-schema', - message: 'Property "type" is not expected to be here.', - path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1', 'type'], - }), - expect.objectContaining({ - code: 'oas3-schema', - message: 'Property "op" is not expected to be here.', - path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1', 'op'], - }), - expect.objectContaining({ - code: 'invalid-ref', - }), - expect.objectContaining({ - code: 'invalid-ref', - }), - expect.objectContaining({ - code: 'oas3-valid-schema-example', - message: '"example" property type must be number', - path: ['components', 'schemas', 'foo', 'example'], - }), - ]); - }); }); diff --git a/packages/rulesets/src/oas/functions/oasDocumentSchema.ts b/packages/rulesets/src/oas/functions/oasDocumentSchema.ts index a2e731819..a3c20dfb0 100644 --- a/packages/rulesets/src/oas/functions/oasDocumentSchema.ts +++ b/packages/rulesets/src/oas/functions/oasDocumentSchema.ts @@ -1,76 +1,22 @@ -import type { ErrorObject } from 'ajv'; -import { createRulesetFunction, IFunctionResult } from '@stoplight/spectral-core'; -import { schema as schemaFn } from '@stoplight/spectral-functions'; +import betterAjvErrors from '@stoplight/better-ajv-errors'; +import { createRulesetFunction } from '@stoplight/spectral-core'; import { oas2, oas3_1 } from '@stoplight/spectral-formats'; +import type { ErrorObject } from 'ajv'; -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, -}; +import * as schemas from '../schemas/compiled'; -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') - ); +function isRelevant(error: ErrorObject): boolean { + return error.keyword !== 'if'; } -// this is supposed to cover edge cases we need to cover manually, when it's impossible to detect the most appropriate error, i.e. oneOf consisting of more than 3 members, etc. -// note, more errors can be included if certain messages reported by AJV are not quite meaningful -const ERROR_MAP = [ - { - path: /^components\/securitySchemes\/[^/]+$/, - message: 'Invalid security scheme', - }, -]; - -// The function removes irrelevant (aka misleading, confusing, useless, whatever you call it) errors. -// There are a few exceptions, i.e. security components I covered manually, -// yet apart from them we usually deal with a relatively simple scenario that can be literally expressed as: "either proper value of $ref property". -// The $ref part is never going to be interesting for us, because both oas-schema rules operate on resolved content, so we won't have any $refs left. -// As you can see, what we deal here wit is actually not really oneOf anymore - it's always the first member of oneOf we match against. -// That being said, we always strip both oneOf and $ref, since we are always interested in the first error. -export function prepareResults(errors: ErrorObject[]): void { - // Update additionalProperties errors to make them more precise and prevent them from being treated as duplicates - for (const error of errors) { - if (error.keyword === 'additionalProperties') { - error.instancePath = `${error.instancePath}/${String(error.params['additionalProperty'])}`; - } - } - - for (let i = 0; i < errors.length; i++) { - const error = errors[i]; - - if (i + 1 < errors.length && errors[i + 1].instancePath === error.instancePath) { - errors.splice(i + 1, 1); - i--; - } else if (i > 0 && shouldIgnoreError(error) && errors[i - 1].instancePath.startsWith(error.instancePath)) { - errors.splice(i, 1); - i--; - } +function processError(error: ErrorObject): ErrorObject { + if (error.keyword === 'additionalProperties') { + return Object.assign({}, error, { + instancePath: `${error.instancePath}/${String(error.params['additionalProperty'])}`, + }); } -} - -function applyManualReplacements(errors: IFunctionResult[]): void { - for (const error of errors) { - if (error.path === void 0) continue; - const joinedPath = error.path.join('/'); - - for (const mappedError of ERROR_MAP) { - if (mappedError.path.test(joinedPath)) { - error.message = mappedError.message; - break; - } - } - } + return error; } export default createRulesetFunction( @@ -82,18 +28,19 @@ export default createRulesetFunction( const formats = context.document.formats; if (formats === null || formats === void 0) return; - const schema = formats.has(oas2) - ? OAS_SCHEMAS['2.0'] - : formats.has(oas3_1) - ? OAS_SCHEMAS['3.1'] - : OAS_SCHEMAS['3.0']; - - const errors = schemaFn(targetVal, { allErrors: true, schema, prepareResults }, context); + const validator = formats.has(oas2) ? schemas.oas2_0 : formats.has(oas3_1) ? schemas.oas3_1 : schemas.oas3_0; - if (Array.isArray(errors)) { - applyManualReplacements(errors); - } + validator(targetVal); + // @ts-expect-error: validator typings aren't fully correct + const { errors } = validator; + if (!Array.isArray(errors)) return; - return errors; + return betterAjvErrors({}, errors.filter(isRelevant).map(processError), { + propertyPath: context.path, + targetValue: targetVal, + }).map(({ suggestion, error, path: errorPath }) => ({ + message: suggestion !== void 0 ? `${error}. ${suggestion}` : error, + path: [...context.path, ...(errorPath !== '' ? errorPath.replace(/^\//, '').split('/') : [])], + })); }, ); diff --git a/packages/rulesets/src/oas/index.ts b/packages/rulesets/src/oas/index.ts index f6f0e0516..09e208afd 100644 --- a/packages/rulesets/src/oas/index.ts +++ b/packages/rulesets/src/oas/index.ts @@ -522,6 +522,7 @@ const ruleset = { recommended: true, formats: [oas2], severity: 0, + resolved: false, given: '$', then: { function: oasDocumentSchema, @@ -678,6 +679,7 @@ const ruleset = { severity: 0, formats: [oas3], recommended: true, + resolved: false, given: '$', then: { function: oasDocumentSchema, diff --git a/packages/rulesets/src/oas/schemas/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/2.0.json b/packages/rulesets/src/oas/schemas/v2.0.json similarity index 98% rename from packages/rulesets/src/oas/schemas/2.0.json rename to packages/rulesets/src/oas/schemas/v2.0.json index 7fa341242..3c0c73047 100644 --- a/packages/rulesets/src/oas/schemas/2.0.json +++ b/packages/rulesets/src/oas/schemas/v2.0.json @@ -1,7 +1,7 @@ { "title": "A JSON Schema for Swagger 2.0 API.", "$id": "http://swagger.io/v2/schema.json#", - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": ["swagger", "info", "paths"], "additionalProperties": false, @@ -1029,7 +1029,6 @@ "$ref": "http://json-schema.org/draft-04/schema#/definitions/stringArray" }, "type": { - "type": "string", "enum": ["file"] }, "readOnly": { @@ -1048,7 +1047,6 @@ "additionalProperties": false, "properties": { "type": { - "type": "string", "enum": ["string", "number", "integer", "boolean", "array"] }, "format": { @@ -1211,7 +1209,6 @@ "required": ["type"], "properties": { "type": { - "type": "string", "enum": ["basic"] }, "description": { @@ -1230,14 +1227,12 @@ "required": ["type", "name", "in"], "properties": { "type": { - "type": "string", "enum": ["apiKey"] }, "name": { "type": "string" }, "in": { - "type": "string", "enum": ["header", "query"] }, "description": { @@ -1256,11 +1251,9 @@ "required": ["type", "flow", "authorizationUrl", "scopes"], "properties": { "type": { - "type": "string", "enum": ["oauth2"] }, "flow": { - "type": "string", "enum": ["implicit"] }, "scopes": { @@ -1286,11 +1279,9 @@ "required": ["type", "flow", "tokenUrl", "scopes"], "properties": { "type": { - "type": "string", "enum": ["oauth2"] }, "flow": { - "type": "string", "enum": ["password"] }, "scopes": { @@ -1316,12 +1307,10 @@ "required": ["type", "flow", "tokenUrl", "scopes"], "properties": { "type": { - "type": "string", - "enum": ["oauth2"] + "const": "oauth2" }, "flow": { - "type": "string", - "enum": ["application"] + "const": "application" }, "scopes": { "$ref": "#/definitions/oauth2Scopes" @@ -1346,12 +1335,10 @@ "required": ["type", "flow", "authorizationUrl", "tokenUrl", "scopes"], "properties": { "type": { - "type": "string", - "enum": ["oauth2"] + "const": "oauth2" }, "flow": { - "type": "string", - "enum": ["accessCode"] + "const": "accessCode" }, "scopes": { "$ref": "#/definitions/oauth2Scopes" @@ -1407,18 +1394,15 @@ "type": "array", "description": "The transfer protocol of the API.", "items": { - "type": "string", "enum": ["http", "https", "ws", "wss"] }, "uniqueItems": true }, "collectionFormat": { - "type": "string", "enum": ["csv", "ssv", "tsv", "pipes"], "default": "csv" }, "collectionFormatWithMulti": { - "type": "string", "enum": ["csv", "ssv", "tsv", "pipes", "multi"], "default": "csv" }, diff --git a/packages/rulesets/src/oas/schemas/3.0.json b/packages/rulesets/src/oas/schemas/v3.0.json similarity index 73% rename from packages/rulesets/src/oas/schemas/3.0.json rename to packages/rulesets/src/oas/schemas/v3.0.json index 3e500737d..867ee6e48 100644 --- a/packages/rulesets/src/oas/schemas/3.0.json +++ b/packages/rulesets/src/oas/schemas/v3.0.json @@ -1,6 +1,6 @@ { "$id": "https://spec.openapis.org/oas/3.0/schema/2019-04-02", - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "description": "Validation schema for OpenAPI Specification 3.0.X.", "type": "object", "required": ["openapi", "info", "paths"], @@ -173,14 +173,16 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } } } }, @@ -188,14 +190,16 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/Response" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Response" + } } } }, @@ -203,14 +207,16 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/Parameter" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Parameter" + } } } }, @@ -218,14 +224,16 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/Example" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Example" + } } } }, @@ -233,14 +241,16 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/RequestBody" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/RequestBody" + } } } }, @@ -248,14 +258,16 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/Header" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Header" + } } } }, @@ -263,14 +275,16 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/SecurityScheme" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/SecurityScheme" + } } } }, @@ -278,14 +292,16 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/Link" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Link" + } } } }, @@ -293,14 +309,16 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/Callback" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Callback" + } } } } @@ -384,93 +402,110 @@ "uniqueItems": false }, "type": { - "type": "string", "enum": ["array", "boolean", "integer", "number", "object", "string"] }, "not": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } }, "allOf": { "type": "array", "items": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } } }, "oneOf": { "type": "array", "items": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } } }, "anyOf": { "type": "array", "items": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - } - ] - } - }, - "items": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" + "if": { + "type": "object", + "required": ["$ref"] }, - { + "then": { "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" } - ] + } + }, + "items": { + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } }, "properties": { "type": "object", "additionalProperties": { + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } + } + }, + "additionalProperties": { + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { "oneOf": [ { "$ref": "#/definitions/Schema" }, { - "$ref": "#/definitions/Reference" + "type": "boolean" } ] - } - }, - "additionalProperties": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - }, - { - "type": "boolean" - } - ], + }, "default": true }, "description": { @@ -564,14 +599,16 @@ "headers": { "type": "object", "additionalProperties": { - "oneOf": [ - { - "$ref": "#/definitions/Header" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Header" + } } }, "content": { @@ -583,14 +620,16 @@ "links": { "type": "object", "additionalProperties": { - "oneOf": [ - { - "$ref": "#/definitions/Link" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Link" + } } } }, @@ -603,27 +642,31 @@ "type": "object", "properties": { "schema": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } }, "example": {}, "examples": { "type": "object", "additionalProperties": { - "oneOf": [ - { - "$ref": "#/definitions/Example" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Example" + } } }, "encoding": { @@ -682,8 +725,7 @@ "default": false }, "style": { - "type": "string", - "enum": ["simple"], + "const": "simple", "default": "simple" }, "explode": { @@ -694,14 +736,16 @@ "default": false }, "schema": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } }, "content": { "type": "object", @@ -715,14 +759,16 @@ "examples": { "type": "object", "additionalProperties": { - "oneOf": [ - { - "$ref": "#/definitions/Example" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Example" + } } } }, @@ -770,14 +816,16 @@ "parameters": { "type": "array", "items": { - "oneOf": [ - { - "$ref": "#/definitions/Parameter" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Parameter" + } }, "uniqueItems": true } @@ -815,26 +863,30 @@ "parameters": { "type": "array", "items": { - "oneOf": [ - { - "$ref": "#/definitions/Parameter" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Parameter" + } }, "uniqueItems": true }, "requestBody": { - "oneOf": [ - { - "$ref": "#/definitions/RequestBody" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/RequestBody" + } }, "responses": { "$ref": "#/definitions/Responses" @@ -842,14 +894,16 @@ "callbacks": { "type": "object", "additionalProperties": { - "oneOf": [ - { - "$ref": "#/definitions/Callback" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Callback" + } } }, "deprecated": { @@ -878,26 +932,30 @@ "type": "object", "properties": { "default": { - "oneOf": [ - { - "$ref": "#/definitions/Response" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Response" + } } }, "patternProperties": { "^[1-5](?:\\d{2}|XX)$": { - "oneOf": [ - { - "$ref": "#/definitions/Response" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Response" + } }, "^x-": {} }, @@ -957,15 +1015,23 @@ }, "SchemaXORContent": { "description": "Schema and content are mutually exclusive, at least one is required", + "errorMessage": { + "not": "Schema and content are mutually exclusive, at least one is required" + }, "not": { "required": ["schema", "content"] }, - "oneOf": [ - { - "required": ["schema"] + "if": { + "type": "object", + "required": ["schema"] + }, + "then": true, + "else": { + "if": { + "type": "object", + "required": ["content"] }, - { - "required": ["content"], + "then": { "description": "Some properties are not allowed if content is present", "allOf": [ { @@ -994,8 +1060,14 @@ } } ] + }, + "else": { + "not": true, + "errorMessage": { + "not": "\"schema\" or \"content\" must be present" + } } - ] + } }, "Parameter": { "type": "object", @@ -1032,14 +1104,16 @@ "default": false }, "schema": { - "oneOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Schema" + } }, "content": { "type": "object", @@ -1053,14 +1127,16 @@ "examples": { "type": "object", "additionalProperties": { - "oneOf": [ - { - "$ref": "#/definitions/Example" - }, - { - "$ref": "#/definitions/Reference" - } - ] + "if": { + "type": "object", + "required": ["$ref"] + }, + "then": { + "$ref": "#/definitions/Reference" + }, + "else": { + "$ref": "#/definitions/Example" + } } } }, @@ -1068,7 +1144,7 @@ "^x-": {} }, "additionalProperties": false, - "required": ["name", "in"], + "required": ["name"], "allOf": [ { "$ref": "#/definitions/ExampleXORExamples" @@ -1083,60 +1159,101 @@ }, "ParameterLocation": { "description": "Parameter location", - "oneOf": [ - { - "description": "Parameter in path", - "required": ["required"], + "type": "object", + "if": { + "type": "object", + "properties": { + "in": { + "const": "path" + } + }, + "required": ["in"] + }, + "then": { + "description": "Parameter in path", + "required": ["required"], + "properties": { + "style": { + "enum": ["matrix", "label", "simple"], + "default": "simple" + }, + "required": { + "const": true + } + } + }, + "else": { + "if": { + "type": "object", "properties": { "in": { - "enum": ["path"] - }, - "style": { - "enum": ["matrix", "label", "simple"], - "default": "simple" - }, - "required": { - "enum": [true] + "const": "query" } - } + }, + "required": ["in"] }, - { + "then": { "description": "Parameter in query", "properties": { - "in": { - "enum": ["query"] - }, "style": { "enum": ["form", "spaceDelimited", "pipeDelimited", "deepObject"], "default": "form" } } }, - { - "description": "Parameter in header", - "properties": { - "in": { - "enum": ["header"] + "else": { + "if": { + "type": "object", + "properties": { + "in": { + "const": "header" + } }, - "style": { - "enum": ["simple"], - "default": "simple" + "required": ["in"] + }, + "then": { + "description": "Parameter in header", + "properties": { + "style": { + "const": "simple", + "default": "simple" + } } - } - }, - { - "description": "Parameter in cookie", - "properties": { - "in": { - "enum": ["cookie"] + }, + "else": { + "if": { + "type": "object", + "properties": { + "in": { + "const": "cookie" + } + }, + "required": ["in"] }, - "style": { - "enum": ["form"], - "default": "form" + "then": { + "description": "Parameter in cookie", + "properties": { + "style": { + "const": "form", + "default": "form" + } + } + }, + "else": { + "type": "object", + "properties": { + "in": { + "enum": ["path", "query", "header", "cookie"] + } + }, + "required": ["in"], + "errorMessage": { + "required": "Parameter must have a valid \"in\" property" + } } } } - ] + } }, "RequestBody": { "type": "object", @@ -1162,34 +1279,85 @@ "additionalProperties": false }, "SecurityScheme": { - "oneOf": [ - { - "$ref": "#/definitions/APIKeySecurityScheme" + "if": { + "type": "object", + "properties": { + "type": { + "const": "apiKey" + } }, - { - "$ref": "#/definitions/HTTPSecurityScheme" + "required": ["type"] + }, + "then": { + "$ref": "#/definitions/APIKeySecurityScheme" + }, + "else": { + "if": { + "type": "object", + "properties": { + "type": { + "const": "apiKey" + } + }, + "required": ["type"] }, - { - "$ref": "#/definitions/OAuth2SecurityScheme" + "then": { + "$ref": "#/definitions/HTTPSecurityScheme" }, - { - "$ref": "#/definitions/OpenIdConnectSecurityScheme" + "else": { + "if": { + "type": "object", + "properties": { + "type": { + "const": "oauth2" + } + }, + "required": ["type"] + }, + "then": { + "$ref": "#/definitions/OAuth2SecurityScheme" + }, + "else": { + "if": { + "type": "object", + "properties": { + "type": { + "const": "openIdConnect" + } + }, + "required": ["type"] + }, + "then": { + "$ref": "#/definitions/OpenIdConnectSecurityScheme" + }, + "else": { + "type": "object", + "properties": { + "type": { + "enum": ["apiKey", "http", "oauth2", "openIdConnect"] + } + }, + "required": ["type"], + "errorMessage": { + "required": "Security scheme must have a valid type", + "type": "Invalid security scheme" + } + } + } } - ] + } }, "APIKeySecurityScheme": { "type": "object", "required": ["type", "name", "in"], "properties": { "type": { - "type": "string", - "enum": ["apiKey"] + "const": "apiKey" }, "name": { "type": "string" }, "in": { - "type": "string", "enum": ["header", "query", "cookie"] }, "description": { @@ -1216,7 +1384,7 @@ }, "type": { "type": "string", - "enum": ["http"] + "const": "http" } }, "patternProperties": { @@ -1228,7 +1396,7 @@ "description": "Bearer", "properties": { "scheme": { - "enum": ["bearer"] + "const": "bearer" } } }, @@ -1240,7 +1408,7 @@ "properties": { "scheme": { "not": { - "enum": ["bearer"] + "const": "bearer" } } } @@ -1252,8 +1420,7 @@ "required": ["type", "flows"], "properties": { "type": { - "type": "string", - "enum": ["oauth2"] + "const": "oauth2" }, "flows": { "$ref": "#/definitions/OAuthFlows" @@ -1272,8 +1439,7 @@ "required": ["type", "openIdConnectUrl"], "properties": { "type": { - "type": "string", - "enum": ["openIdConnect"] + "const": "openIdConnect" }, "openIdConnectUrl": { "type": "string", @@ -1462,7 +1628,6 @@ } }, "style": { - "type": "string", "enum": ["form", "spaceDelimited", "pipeDelimited", "deepObject"] }, "explode": { diff --git a/packages/rulesets/src/oas/schemas/v3.1/dialect.schema.json b/packages/rulesets/src/oas/schemas/v3.1/dialect.schema.json new file mode 100644 index 000000000..3b2064572 --- /dev/null +++ b/packages/rulesets/src/oas/schemas/v3.1/dialect.schema.json @@ -0,0 +1,25 @@ +{ + "$id": "https://spec.openapis.org/oas/3.1/dialect/base", + "$schema": "https://json-schema.org/draft/2020-12/schema", + + "title": "OpenAPI 3.1 Schema Object Dialect", + "description": "A JSON Schema dialect describing schemas found in OpenAPI documents", + + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/content": true, + "https://spec.openapis.org/oas/3.1/vocab/base": false + }, + + "$dynamicAnchor": "meta", + + "allOf": [ + { "$ref": "https://json-schema.org/draft/2020-12/schema" }, + { "$ref": "https://spec.openapis.org/oas/3.1/meta/base" } + ] +} diff --git a/packages/rulesets/src/oas/schemas/3.1.json b/packages/rulesets/src/oas/schemas/v3.1/index.json similarity index 82% rename from packages/rulesets/src/oas/schemas/3.1.json rename to packages/rulesets/src/oas/schemas/v3.1/index.json index eab8be0aa..51b7c14de 100644 --- a/packages/rulesets/src/oas/schemas/3.1.json +++ b/packages/rulesets/src/oas/schemas/v3.1/index.json @@ -1,6 +1,7 @@ { "$id": "https://spec.openapis.org/oas/3.1/schema/2021-09-28", "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "The description of OpenAPI v3.1.x documents without schema validation, as defined by https://spec.openapis.org/oas/v3.1.0", "type": "object", "properties": { "openapi": { @@ -19,7 +20,12 @@ "type": "array", "items": { "$ref": "#/$defs/server" - } + }, + "default": [ + { + "url": "/" + } + ] }, "paths": { "$ref": "#/$defs/paths" @@ -50,15 +56,20 @@ } }, "required": ["openapi", "info"], + "errorMessage": { + "anyOf": "The document must have either \"paths\", \"webhooks\" or \"components\"" + }, "anyOf": [ { - "required": ["paths"], - "errorMessage": "The document must have either \"paths\", \"webhooks\" or \"components\"" + "errorMessage": "The document must have either \"paths\", \"webhooks\" or \"components\"", + "required": ["paths"] }, { + "errorMessage": "The document must have either \"paths\", \"webhooks\" or \"components\"", "required": ["components"] }, { + "errorMessage": "The document must have either \"paths\", \"webhooks\" or \"components\"", "required": ["webhooks"] } ], @@ -66,6 +77,7 @@ "unevaluatedProperties": false, "$defs": { "info": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#info-object", "type": "object", "properties": { "title": { @@ -78,7 +90,8 @@ "type": "string" }, "termsOfService": { - "type": "string" + "type": "string", + "format": "uri" }, "contact": { "$ref": "#/$defs/contact" @@ -95,22 +108,26 @@ "unevaluatedProperties": false }, "contact": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#contact-object", "type": "object", "properties": { "name": { "type": "string" }, "url": { - "type": "string" + "type": "string", + "format": "uri" }, "email": { - "type": "string" + "type": "string", + "format": "email" } }, "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "license": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#license-object", "type": "object", "properties": { "name": { @@ -125,23 +142,22 @@ } }, "required": ["name"], - "oneOf": [ - { - "required": ["identifier"] - }, - { - "required": ["url"] + "dependentSchemas": { + "identifier": { + "not": { + "required": ["url"] + } } - ], + }, "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "server": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#server-object", "type": "object", "properties": { "url": { - "type": "string", - "format": "uri-template" + "type": "string" }, "description": { "type": "string" @@ -158,6 +174,7 @@ "unevaluatedProperties": false }, "server-variable": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#server-variable-object", "type": "object", "properties": { "enum": { @@ -179,12 +196,13 @@ "unevaluatedProperties": false }, "components": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#components-object", "type": "object", "properties": { "schemas": { "type": "object", "additionalProperties": { - "$ref": "#/$defs/schema" + "$ref": "https://spec.openapis.org/oas/3.1/dialect/base" } }, "responses": { @@ -254,6 +272,7 @@ "unevaluatedProperties": false }, "paths": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#paths-object", "type": "object", "patternProperties": { "^/": { @@ -264,6 +283,7 @@ "unevaluatedProperties": false }, "path-item": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#path-item-object", "type": "object", "properties": { "summary": { @@ -283,10 +303,29 @@ "items": { "$ref": "#/$defs/parameter-or-reference" } - } - }, - "patternProperties": { - "^(get|put|post|delete|options|head|patch|trace)$": { + }, + "get": { + "$ref": "#/$defs/operation" + }, + "put": { + "$ref": "#/$defs/operation" + }, + "post": { + "$ref": "#/$defs/operation" + }, + "delete": { + "$ref": "#/$defs/operation" + }, + "options": { + "$ref": "#/$defs/operation" + }, + "head": { + "$ref": "#/$defs/operation" + }, + "patch": { + "$ref": "#/$defs/operation" + }, + "trace": { "$ref": "#/$defs/operation" } }, @@ -306,6 +345,7 @@ } }, "operation": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#operation-object", "type": "object", "properties": { "tags": { @@ -365,6 +405,7 @@ "unevaluatedProperties": false }, "external-documentation": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#external-documentation-object", "type": "object", "properties": { "description": { @@ -380,6 +421,7 @@ "unevaluatedProperties": false }, "parameter": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#parameter-object", "type": "object", "properties": { "name": { @@ -399,18 +441,16 @@ "default": false, "type": "boolean" }, - "allowEmptyValue": { - "default": false, - "type": "boolean" - }, "schema": { - "$ref": "#/$defs/schema" + "$ref": "https://spec.openapis.org/oas/3.1/dialect/base" }, "content": { - "$ref": "#/$defs/content" + "$ref": "#/$defs/content", + "minProperties": 1, + "maxProperties": 1 } }, - "required": ["in"], + "required": ["name", "in"], "oneOf": [ { "required": ["schema"] @@ -419,6 +459,22 @@ "required": ["content"] } ], + "if": { + "properties": { + "in": { + "const": "query" + } + }, + "required": ["in"] + }, + "then": { + "properties": { + "allowEmptyValue": { + "default": false, + "type": "boolean" + } + } + }, "dependentSchemas": { "schema": { "properties": { @@ -427,10 +483,6 @@ }, "explode": { "type": "boolean" - }, - "allowReserved": { - "default": false, - "type": "boolean" } }, "allOf": [ @@ -450,7 +502,7 @@ "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-cookie" }, { - "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-form" + "$ref": "#/$defs/styles-for-form" } ], "$defs": { @@ -465,9 +517,6 @@ }, "then": { "properties": { - "name": { - "pattern": "[^/#?]+$" - }, "style": { "default": "simple", "enum": ["matrix", "label", "simple"] @@ -511,6 +560,10 @@ "style": { "default": "form", "enum": ["form", "spaceDelimited", "pipeDelimited", "deepObject"] + }, + "allowReserved": { + "default": false, + "type": "boolean" } } } @@ -532,30 +585,6 @@ } } } - }, - "styles-for-form": { - "if": { - "properties": { - "style": { - "const": "form" - } - }, - "required": ["style"] - }, - "then": { - "properties": { - "explode": { - "default": true - } - } - }, - "else": { - "properties": { - "explode": { - "default": false - } - } - } } } } @@ -576,6 +605,7 @@ } }, "request-body": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#request-body-object", "type": "object", "properties": { "description": { @@ -606,6 +636,7 @@ } }, "content": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#fixed-fields-10", "type": "object", "additionalProperties": { "$ref": "#/$defs/media-type" @@ -615,10 +646,11 @@ } }, "media-type": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#media-type-object", "type": "object", "properties": { "schema": { - "$ref": "#/$defs/schema" + "$ref": "https://spec.openapis.org/oas/3.1/dialect/base" }, "encoding": { "type": "object", @@ -638,6 +670,7 @@ "unevaluatedProperties": false }, "encoding": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#encoding-object", "type": "object", "properties": { "contentType": { @@ -667,38 +700,13 @@ "$ref": "#/$defs/specification-extensions" }, { - "$ref": "#/$defs/encoding/$defs/explode-default" + "$ref": "#/$defs/styles-for-form" } ], - "unevaluatedProperties": false, - "$defs": { - "explode-default": { - "if": { - "properties": { - "style": { - "const": "form" - } - }, - "required": ["style"] - }, - "then": { - "properties": { - "explode": { - "default": true - } - } - }, - "else": { - "properties": { - "explode": { - "default": false - } - } - } - } - } + "unevaluatedProperties": false }, "responses": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#responses-object", "type": "object", "properties": { "default": { @@ -710,10 +718,21 @@ "$ref": "#/$defs/response-or-reference" } }, + "minProperties": 1, "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false + "unevaluatedProperties": false, + "if": { + "$comment": "either default, or at least one response code property must exist", + "patternProperties": { + "^[1-5](?:[0-9]{2}|XX)$": false + } + }, + "then": { + "required": ["default"] + } }, "response": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#response-object", "type": "object", "properties": { "description": { @@ -752,6 +771,7 @@ } }, "callbacks": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#callback-object", "type": "object", "$ref": "#/$defs/specification-extensions", "additionalProperties": { @@ -771,6 +791,7 @@ } }, "example": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#example-object", "type": "object", "properties": { "summary": { @@ -785,6 +806,9 @@ "format": "uri" } }, + "not": { + "required": ["value", "externalValue"] + }, "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, @@ -801,13 +825,16 @@ } }, "link": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#link-object", "type": "object", "properties": { "operationRef": { "type": "string", "format": "uri-reference" }, - "operationId": true, + "operationId": { + "type": "string" + }, "parameters": { "$ref": "#/$defs/map-of-strings" }, @@ -843,6 +870,7 @@ } }, "header": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#header-object", "type": "object", "properties": { "description": { @@ -857,10 +885,12 @@ "type": "boolean" }, "schema": { - "$ref": "#/$defs/schema" + "$ref": "https://spec.openapis.org/oas/3.1/dialect/base" }, "content": { - "$ref": "#/$defs/content" + "$ref": "#/$defs/content", + "minProperties": 1, + "maxProperties": 1 } }, "oneOf": [ @@ -902,6 +932,7 @@ } }, "tag": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#tag-object", "type": "object", "properties": { "name": { @@ -919,6 +950,7 @@ "unevaluatedProperties": false }, "reference": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#reference-object", "type": "object", "properties": { "$ref": { @@ -931,14 +963,15 @@ "description": { "type": "string" } - }, - "unevaluatedProperties": false + } }, "schema": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#schema-object", "$dynamicAnchor": "meta", "type": ["object", "boolean"] }, "security-scheme": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#security-scheme-object", "type": "object", "properties": { "type": { @@ -1105,10 +1138,12 @@ "type": "object", "properties": { "authorizationUrl": { - "type": "string" + "type": "string", + "format": "uri" }, "refreshUrl": { - "type": "string" + "type": "string", + "format": "uri" }, "scopes": { "$ref": "#/$defs/map-of-strings" @@ -1122,10 +1157,12 @@ "type": "object", "properties": { "tokenUrl": { - "type": "string" + "type": "string", + "format": "uri" }, "refreshUrl": { - "type": "string" + "type": "string", + "format": "uri" }, "scopes": { "$ref": "#/$defs/map-of-strings" @@ -1139,10 +1176,12 @@ "type": "object", "properties": { "tokenUrl": { - "type": "string" + "type": "string", + "format": "uri" }, "refreshUrl": { - "type": "string" + "type": "string", + "format": "uri" }, "scopes": { "$ref": "#/$defs/map-of-strings" @@ -1156,13 +1195,16 @@ "type": "object", "properties": { "authorizationUrl": { - "type": "string" + "type": "string", + "format": "uri" }, "tokenUrl": { - "type": "string" + "type": "string", + "format": "uri" }, "refreshUrl": { - "type": "string" + "type": "string", + "format": "uri" }, "scopes": { "$ref": "#/$defs/map-of-strings" @@ -1175,6 +1217,7 @@ } }, "security-requirement": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#security-requirement-object", "type": "object", "additionalProperties": { "type": "array", @@ -1184,6 +1227,7 @@ } }, "specification-extensions": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#specification-extensions", "patternProperties": { "^x-": true } @@ -1204,6 +1248,30 @@ "additionalProperties": { "type": "string" } + }, + "styles-for-form": { + "if": { + "properties": { + "style": { + "const": "form" + } + }, + "required": ["style"] + }, + "then": { + "properties": { + "explode": { + "default": true + } + } + }, + "else": { + "properties": { + "explode": { + "default": false + } + } + } } } } diff --git a/packages/rulesets/src/oas/schemas/v3.1/meta.schema.json b/packages/rulesets/src/oas/schemas/v3.1/meta.schema.json new file mode 100644 index 000000000..e8a20ef48 --- /dev/null +++ b/packages/rulesets/src/oas/schemas/v3.1/meta.schema.json @@ -0,0 +1,87 @@ +{ + "$id": "https://spec.openapis.org/oas/3.1/meta/base", + "$schema": "https://json-schema.org/draft/2020-12/schema", + + "title": "OAS Base vocabulary", + "description": "A JSON Schema Vocabulary used in the OpenAPI Schema Dialect", + + "$vocabulary": { + "https://spec.openapis.org/oas/3.1/vocab/base": true + }, + + "$dynamicAnchor": "meta", + + "type": ["object", "boolean"], + "properties": { + "example": true, + "discriminator": { "$ref": "#/$defs/discriminator" }, + "externalDocs": { "$ref": "#/$defs/external-docs" }, + "xml": { "$ref": "#/$defs/xml" } + }, + + "$defs": { + "extensible": { + "patternProperties": { + "^x-": true + } + }, + + "discriminator": { + "$ref": "#/$defs/extensible", + "type": "object", + "properties": { + "propertyName": { + "type": "string" + }, + "mapping": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["propertyName"], + "unevaluatedProperties": false + }, + + "external-docs": { + "$ref": "#/$defs/extensible", + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri-reference" + }, + "description": { + "type": "string" + } + }, + "required": ["url"], + "unevaluatedProperties": false + }, + + "xml": { + "$ref": "#/$defs/extensible", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string", + "format": "uri" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean" + }, + "wrapped": { + "type": "boolean" + } + }, + "unevaluatedProperties": false + } + } +} diff --git a/test-harness/scenarios/oas3-schema.scenario b/test-harness/scenarios/oas3-schema.scenario index 552835f71..8f054e6a7 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 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 properties. paths./address.get.responses -✖ 9 problems (9 errors, 0 warnings, 0 infos, 0 hints) +✖ 10 problems (10 errors, 0 warnings, 0 infos, 0 hints) diff --git a/test-harness/scenarios/oas3.1/petstore.scenario b/test-harness/scenarios/oas3.1/petstore.scenario index d5c776053..a7bb6f96b 100644 --- a/test-harness/scenarios/oas3.1/petstore.scenario +++ b/test-harness/scenarios/oas3.1/petstore.scenario @@ -135,7 +135,8 @@ paths: description: OK summary: Create or replace your avatar. parameters: - - schema: + - name: avatar + schema: type: string in: header components: diff --git a/yarn.lock b/yarn.lock index 4782b56b6..1f7d9cef5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1755,14 +1755,14 @@ __metadata: languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.2": - version: 0.3.2 - resolution: "@jridgewell/gen-mapping@npm:0.3.2" +"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.2": + version: 0.3.3 + resolution: "@jridgewell/gen-mapping@npm:0.3.3" dependencies: "@jridgewell/set-array": ^1.0.1 "@jridgewell/sourcemap-codec": ^1.4.10 "@jridgewell/trace-mapping": ^0.3.9 - checksum: 1832707a1c476afebe4d0fbbd4b9434fdb51a4c3e009ab1e9938648e21b7a97049fa6009393bdf05cab7504108413441df26d8a3c12193996e65493a4efb6882 + checksum: 4a74944bd31f22354fc01c3da32e83c19e519e3bbadafa114f6da4522ea77dd0c2842607e923a591d60a76699d819a2fbb6f3552e277efdb9b58b081390b60ab languageName: node linkType: hard @@ -1780,6 +1780,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/source-map@npm:^0.3.3": + version: 0.3.5 + resolution: "@jridgewell/source-map@npm:0.3.5" + dependencies: + "@jridgewell/gen-mapping": ^0.3.0 + "@jridgewell/trace-mapping": ^0.3.9 + checksum: 1ad4dec0bdafbade57920a50acec6634f88a0eb735851e0dda906fa9894e7f0549c492678aad1a10f8e144bfe87f238307bf2a914a1bc85b7781d345417e9f6f + languageName: node + linkType: hard + "@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14": version: 1.4.15 resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" @@ -2838,11 +2848,13 @@ __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 lodash: ~4.17.21 + terser: ^5.26.0 tslib: ^2.3.0 languageName: unknown linkType: soft @@ -3627,12 +3639,12 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.1.0, acorn@npm:^8.4.1, acorn@npm:^8.8.0": - version: 8.8.0 - resolution: "acorn@npm:8.8.0" +"acorn@npm:^8.1.0, acorn@npm:^8.4.1, acorn@npm:^8.8.0, acorn@npm:^8.8.2": + version: 8.11.3 + resolution: "acorn@npm:8.11.3" bin: acorn: bin/acorn - checksum: 7270ca82b242eafe5687a11fea6e088c960af712683756abf0791b68855ea9cace3057bd5e998ffcef50c944810c1e0ca1da526d02b32110e13c722aa959afdc + checksum: 76d8e7d559512566b43ab4aadc374f11f563f0a9e21626dd59cb2888444e9445923ae9f3699972767f18af61df89cd89f5eaaf772d1327b055b45cb829b4a88c languageName: node linkType: hard @@ -3722,15 +3734,15 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.0.0, ajv@npm:^8.6.0, ajv@npm:^8.6.3, ajv@npm:^8.8.2": - version: 8.8.2 - resolution: "ajv@npm:8.8.2" +"ajv@npm:^8.0.0, ajv@npm:^8.12.0, ajv@npm:^8.6.0, ajv@npm:^8.6.3": + version: 8.12.0 + resolution: "ajv@npm:8.12.0" dependencies: fast-deep-equal: ^3.1.1 json-schema-traverse: ^1.0.0 require-from-string: ^2.0.2 uri-js: ^4.2.2 - checksum: 90849ef03c4f4f7051d15f655120137b89e3205537d683beebd39d95f40c0ca00ea8476cd999602d2f433863e7e4bf1b81d1869d1e07f4dcf56d71b6430a605c + checksum: 4dc13714e316e67537c8b31bc063f99a1d9d9a497eb4bbd55191ac0dcd5e4985bbb71570352ad6f1e76684fb6d790928f96ba3b2d4fd6e10024be9612fe3f001 languageName: node linkType: hard @@ -4854,6 +4866,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^2.20.0": + version: 2.20.3 + resolution: "commander@npm:2.20.3" + checksum: ab8c07884e42c3a8dbc5dd9592c606176c7eb5c1ca5ff274bcf907039b2c41de3626f684ea75ccf4d361ba004bbaff1f577d5384c155f3871e456bdf27becf9e + languageName: node + linkType: hard + "commander@npm:^8.2.0": version: 8.2.0 resolution: "commander@npm:8.2.0" @@ -5602,6 +5621,13 @@ __metadata: languageName: node linkType: hard +"duplexer@npm:^0.1.2": + version: 0.1.2 + resolution: "duplexer@npm:0.1.2" + checksum: 62ba61a830c56801db28ff6305c7d289b6dc9f859054e8c982abd8ee0b0a14d2e9a8e7d086ffee12e868d43e2bbe8a964be55ddbd8c8957714c87373c7a4f9b0 + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -6919,6 +6945,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" @@ -11919,13 +11954,13 @@ __metadata: languageName: node linkType: hard -"source-map-support@npm:^0.5.17": - version: 0.5.19 - resolution: "source-map-support@npm:0.5.19" +"source-map-support@npm:^0.5.17, source-map-support@npm:~0.5.20": + version: 0.5.21 + resolution: "source-map-support@npm:0.5.21" dependencies: buffer-from: ^1.0.0 source-map: ^0.6.0 - checksum: c72802fdba9cb62b92baef18cc14cc4047608b77f0353e6c36dd993444149a466a2845332c5540d4a6630957254f0f68f4ef5a0120c33d2e83974c51a05afbac + checksum: 43e98d700d79af1d36f859bdb7318e601dfc918c7ba2e98456118ebc4c4872b327773e5a1df09b0524e9e5063bb18f0934538eace60cca2710d1fa687645d137 languageName: node linkType: hard @@ -12398,6 +12433,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"