diff --git a/projects/openapi-io/inputs/swagger2/spec.yml b/projects/openapi-io/inputs/swagger2/spec.yml new file mode 100644 index 0000000000..f99ae9c68e --- /dev/null +++ b/projects/openapi-io/inputs/swagger2/spec.yml @@ -0,0 +1,24 @@ +swagger: '2.0' +info: + version: '1.2.3' + title: my spec +paths: + /api/users: + get: + produces: ['application/json'] + parameters: + - name: search + in: query + description: search for users + required: true + type: string + responses: + '200': + description: 'response' + schema: + type: object + properties: + id: + type: string + + diff --git a/projects/openapi-io/src/index.ts b/projects/openapi-io/src/index.ts index e84dc186fe..fba9654385 100644 --- a/projects/openapi-io/src/index.ts +++ b/projects/openapi-io/src/index.ts @@ -9,7 +9,10 @@ import { import { ExternalRefHandler } from './parser/types'; import { JsonPath, JsonSchemaSourcemap } from './parser/sourcemap'; import { loadYaml, isYaml, isJson, writeYaml } from './write'; -import { validateOpenApiV3Document } from './validation/validator'; +import { + validateSwaggerV2Document, + validateOpenApiV3Document, +} from './validation/validator'; import { ValidationError, OpenAPIVersionError } from './validation/errors'; import { checkOpenAPIVersion, @@ -38,6 +41,7 @@ export { writeYaml, dereferenceOpenApi, ResolverError, + validateSwaggerV2Document, validateOpenApiV3Document, ValidationError, OpenAPIVersionError, diff --git a/projects/openapi-io/src/validation/__snapshots__/validator.test.ts.snap b/projects/openapi-io/src/validation/__snapshots__/validator.test.ts.snap index 33fad54ee0..944fd57cd1 100644 --- a/projects/openapi-io/src/validation/__snapshots__/validator.test.ts.snap +++ b/projects/openapi-io/src/validation/__snapshots__/validator.test.ts.snap @@ -1,20 +1,50 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`non-strict validation should fail open api doc with invalid status code shape 1`] = ` +exports[`2.x.x validation non-strict validation invalid swagger document should raise errors 1`] = ` +"invalid openapi: paths > /api > get must have required property 'responses' +/paths/~1api/get +invalid openapi: swagger must be equal to one of the allowed values 2.0 +/swagger +invalid openapi: info must have required property 'version' +/info" +`; + +exports[`2.x.x validation strict validation invalid swagger document should raise errors 1`] = ` +"invalid openapi: paths > /api > get > responses must NOT be valid +/paths/~1api/get/responses" +`; + +exports[`3.x.x validation non-strict validation should fail open api doc with invalid status code shape 1`] = ` "invalid openapi: paths > /example > get > responses > 202 must be object /paths/~1example/get/responses/202" `; -exports[`non-strict validation should fail open api doc with no path should throw an error 1`] = ` +exports[`3.x.x validation non-strict validation should fail open api doc with no path should throw an error 1`] = ` "invalid openapi:  must have required property 'paths' " `; -exports[`non-strict validation should fail open api doc without responses 1`] = ` +exports[`3.x.x validation non-strict validation should fail open api doc without responses 1`] = ` "invalid openapi: paths > /example > get must have required property 'responses' /paths/~1example/get" `; +exports[`3.x.x validation strict validation advanced validators run and append their results 1`] = ` +"invalid openapi: paths > /api/users/{userId} > get > responses > 200 > content > application/json > schema > oneOf must NOT have fewer than 1 items +/paths/~1api~1users~1{userId}/get/responses/200/content/application~1json/schema/oneOf +invalid openapi: paths > /api/users/{userId} > get > responses > 200 > content > application/json > schema > anyOf must NOT have fewer than 1 items +/paths/~1api~1users~1{userId}/get/responses/200/content/application~1json/schema/anyOf +invalid openapi: paths > /api/users/{userId} > get > responses > 200 > content > application/json > schema > items must be object +/paths/~1api~1users~1{userId}/get/responses/200/content/application~1json/schema/items +invalid openapi: paths > /api/users/{userId} > get > responses > 200 > content > application/json > schema schema with type "object" cannot also include keywords: items +/paths/~1api~1users~1{userId}/get/responses/200/content/application~1json/schema" +`; + +exports[`3.x.x validation strict validation open api doc with no description in response should throw 1`] = ` +"invalid openapi: paths > /example > get > responses > 200 must have required property 'description' +/paths/~1example/get/responses/200" +`; + exports[`processValidatorErrors 1`] = ` "invalid openapi: paths > /api/users/{userId} > get > responses > 200 > content > application/json > schema > properties > hello > items must be object /paths/~1api~1users~1{userId}/get/responses/200/content/application~1json/schema/properties/hello/items" @@ -38,19 +68,3 @@ exports[`processValidatorErrors attaches the sourcemap 1`] = ` 40 | } 41 | }" `; - -exports[`strict validation advanced validators run and append their results 1`] = ` -"invalid openapi: paths > /api/users/{userId} > get > responses > 200 > content > application/json > schema > oneOf must NOT have fewer than 1 items -/paths/~1api~1users~1{userId}/get/responses/200/content/application~1json/schema/oneOf -invalid openapi: paths > /api/users/{userId} > get > responses > 200 > content > application/json > schema > anyOf must NOT have fewer than 1 items -/paths/~1api~1users~1{userId}/get/responses/200/content/application~1json/schema/anyOf -invalid openapi: paths > /api/users/{userId} > get > responses > 200 > content > application/json > schema > items must be object -/paths/~1api~1users~1{userId}/get/responses/200/content/application~1json/schema/items -invalid openapi: paths > /api/users/{userId} > get > responses > 200 > content > application/json > schema schema with type "object" cannot also include keywords: items -/paths/~1api~1users~1{userId}/get/responses/200/content/application~1json/schema" -`; - -exports[`strict validation open api doc with no description in response should throw 1`] = ` -"invalid openapi: paths > /example > get > responses > 200 must have required property 'description' -/paths/~1example/get/responses/200" -`; diff --git a/projects/openapi-io/src/validation/validation-schemas.ts b/projects/openapi-io/src/validation/validation-schemas.ts index 933b8d64c7..bb4e76bf2a 100644 --- a/projects/openapi-io/src/validation/validation-schemas.ts +++ b/projects/openapi-io/src/validation/validation-schemas.ts @@ -1,3 +1,1574 @@ +export const basic_swagger2_object = { + type: 'object', + required: ['swagger', 'info', 'paths'], + properties: { + swagger: { + type: 'string', + enum: ['2.0'], + }, + info: { + type: 'object', + }, + paths: { + type: 'object', + patternProperties: { + '^/': { + type: 'object', + properties: { + get: { + $ref: '#/definitions/operation', + }, + put: { + $ref: '#/definitions/operation', + }, + post: { + $ref: '#/definitions/operation', + }, + delete: { + $ref: '#/definitions/operation', + }, + options: { + $ref: '#/definitions/operation', + }, + head: { + $ref: '#/definitions/operation', + }, + patch: { + $ref: '#/definitions/operation', + }, + }, + }, + }, + }, + }, + definitions: { + operation: { + type: 'object', + required: ['responses'], + properties: { + responses: { + type: 'object', + additionalProperties: { + type: 'object', + }, + }, + }, + }, + }, +}; + +export const swagger2_schema_object = { + title: 'A JSON Schema for Swagger 2.0 API.', + $id: 'http://swagger.io/v2/schema.json#', + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + required: ['swagger', 'info', 'paths'], + additionalProperties: false, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + properties: { + swagger: { + type: 'string', + enum: ['2.0'], + description: 'The Swagger version of this document.', + }, + info: { + $ref: '#/definitions/info', + }, + host: { + type: 'string', + pattern: '^[^{}/ :\\\\]+(?::\\d+)?$', + description: "The host (name or ip) of the API. Example: 'swagger.io'", + }, + basePath: { + type: 'string', + pattern: '^/', + description: "The base path to the API. Example: '/api'.", + }, + schemes: { + $ref: '#/definitions/schemesList', + }, + consumes: { + description: 'A list of MIME types accepted by the API.', + $ref: '#/definitions/mediaTypeList', + }, + produces: { + description: 'A list of MIME types the API can produce.', + $ref: '#/definitions/mediaTypeList', + }, + paths: { + $ref: '#/definitions/paths', + }, + definitions: { + $ref: '#/definitions/definitions', + }, + parameters: { + $ref: '#/definitions/parameterDefinitions', + }, + responses: { + $ref: '#/definitions/responseDefinitions', + }, + security: { + $ref: '#/definitions/security', + }, + securityDefinitions: { + $ref: '#/definitions/securityDefinitions', + }, + tags: { + type: 'array', + items: { + $ref: '#/definitions/tag', + }, + uniqueItems: true, + }, + externalDocs: { + $ref: '#/definitions/externalDocs', + }, + }, + definitions: { + info: { + type: 'object', + description: 'General information about the API.', + required: ['version', 'title'], + additionalProperties: false, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + properties: { + title: { + type: 'string', + description: 'A unique and precise title of the API.', + }, + version: { + type: 'string', + description: 'A semantic version number of the API.', + }, + description: { + type: 'string', + description: + 'A longer description of the API. Should be different from the title. GitHub Flavored Markdown is allowed.', + }, + termsOfService: { + type: 'string', + description: 'The terms of service for the API.', + }, + contact: { + $ref: '#/definitions/contact', + }, + license: { + $ref: '#/definitions/license', + }, + }, + }, + contact: { + type: 'object', + description: 'Contact information for the owners of the API.', + additionalProperties: false, + properties: { + name: { + type: 'string', + description: + 'The identifying name of the contact person/organization.', + }, + url: { + type: 'string', + description: 'The URL pointing to the contact information.', + format: 'uri', + }, + email: { + type: 'string', + description: 'The email address of the contact person/organization.', + format: 'email', + }, + }, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + }, + license: { + type: 'object', + required: ['name'], + additionalProperties: false, + properties: { + name: { + type: 'string', + description: + "The name of the license type. It's encouraged to use an OSI compatible license.", + }, + url: { + type: 'string', + description: 'The URL pointing to the license.', + format: 'uri', + }, + }, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + }, + paths: { + type: 'object', + description: + "Relative paths to the individual endpoints. They must be relative to the 'basePath'.", + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + '^/': { + $ref: '#/definitions/pathItem', + }, + }, + additionalProperties: false, + }, + definitions: { + type: 'object', + additionalProperties: { + $ref: '#/definitions/schema', + }, + description: + 'One or more JSON objects describing the schemas being consumed and produced by the API.', + }, + parameterDefinitions: { + type: 'object', + additionalProperties: { + $ref: '#/definitions/parameter', + }, + description: 'One or more JSON representations for parameters', + }, + responseDefinitions: { + type: 'object', + additionalProperties: { + $ref: '#/definitions/response', + }, + description: 'One or more JSON representations for parameters', + }, + externalDocs: { + type: 'object', + additionalProperties: false, + description: 'information about external documentation', + required: ['url'], + properties: { + description: { + type: 'string', + }, + url: { + type: 'string', + format: 'uri', + }, + }, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + }, + examples: { + type: 'object', + additionalProperties: true, + }, + mimeType: { + type: 'string', + description: 'The MIME type of the HTTP message.', + }, + operation: { + type: 'object', + required: ['responses'], + additionalProperties: false, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + properties: { + tags: { + type: 'array', + items: { + type: 'string', + }, + uniqueItems: true, + }, + summary: { + type: 'string', + description: 'A brief summary of the operation.', + }, + description: { + type: 'string', + description: + 'A longer description of the operation, GitHub Flavored Markdown is allowed.', + }, + externalDocs: { + $ref: '#/definitions/externalDocs', + }, + operationId: { + type: 'string', + description: 'A unique identifier of the operation.', + }, + produces: { + description: 'A list of MIME types the API can produce.', + $ref: '#/definitions/mediaTypeList', + }, + consumes: { + description: 'A list of MIME types the API can consume.', + $ref: '#/definitions/mediaTypeList', + }, + parameters: { + $ref: '#/definitions/parametersList', + }, + responses: { + $ref: '#/definitions/responses', + }, + schemes: { + $ref: '#/definitions/schemesList', + }, + deprecated: { + type: 'boolean', + default: false, + }, + security: { + $ref: '#/definitions/security', + }, + }, + }, + pathItem: { + type: 'object', + additionalProperties: false, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + properties: { + $ref: { + type: 'string', + }, + get: { + $ref: '#/definitions/operation', + }, + put: { + $ref: '#/definitions/operation', + }, + post: { + $ref: '#/definitions/operation', + }, + delete: { + $ref: '#/definitions/operation', + }, + options: { + $ref: '#/definitions/operation', + }, + head: { + $ref: '#/definitions/operation', + }, + patch: { + $ref: '#/definitions/operation', + }, + parameters: { + $ref: '#/definitions/parametersList', + }, + }, + }, + responses: { + type: 'object', + description: + "Response objects names can either be any valid HTTP status code or 'default'.", + minProperties: 1, + additionalProperties: false, + patternProperties: { + '^([0-9]{3})$|^(default)$': { + $ref: '#/definitions/responseValue', + }, + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + not: { + type: 'object', + additionalProperties: false, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + }, + }, + responseValue: { + oneOf: [ + { + $ref: '#/definitions/response', + }, + { + $ref: '#/definitions/jsonReference', + }, + ], + }, + response: { + type: 'object', + required: ['description'], + properties: { + description: { + type: 'string', + }, + schema: { + oneOf: [ + { + $ref: '#/definitions/schema', + }, + { + $ref: '#/definitions/fileSchema', + }, + ], + }, + headers: { + $ref: '#/definitions/headers', + }, + examples: { + $ref: '#/definitions/examples', + }, + }, + additionalProperties: false, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + }, + headers: { + type: 'object', + additionalProperties: { + $ref: '#/definitions/header', + }, + }, + header: { + type: 'object', + additionalProperties: false, + required: ['type'], + properties: { + type: { + type: 'string', + enum: ['string', 'number', 'integer', 'boolean', 'array'], + }, + format: { + type: 'string', + }, + items: { + $ref: '#/definitions/primitivesItems', + }, + collectionFormat: { + $ref: '#/definitions/collectionFormat', + }, + default: { + $ref: '#/definitions/default', + }, + maximum: { + $ref: '#/definitions/maximum', + }, + exclusiveMaximum: { + $ref: '#/definitions/exclusiveMaximum', + }, + minimum: { + $ref: '#/definitions/minimum', + }, + exclusiveMinimum: { + $ref: '#/definitions/exclusiveMinimum', + }, + maxLength: { + $ref: '#/definitions/maxLength', + }, + minLength: { + $ref: '#/definitions/minLength', + }, + pattern: { + $ref: '#/definitions/pattern', + }, + maxItems: { + $ref: '#/definitions/maxItems', + }, + minItems: { + $ref: '#/definitions/minItems', + }, + uniqueItems: { + $ref: '#/definitions/uniqueItems', + }, + enum: { + $ref: '#/definitions/enum', + }, + multipleOf: { + $ref: '#/definitions/multipleOf', + }, + description: { + type: 'string', + }, + }, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + }, + vendorExtension: { + description: 'Any property starting with x- is valid.', + additionalProperties: true, + additionalItems: true, + }, + bodyParameter: { + type: 'object', + required: ['name', 'in', 'schema'], + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + properties: { + description: { + type: 'string', + description: + 'A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed.', + }, + name: { + type: 'string', + description: 'The name of the parameter.', + }, + in: { + type: 'string', + description: 'Determines the location of the parameter.', + enum: ['body'], + }, + required: { + type: 'boolean', + description: + 'Determines whether or not this parameter is required or optional.', + default: false, + }, + schema: { + $ref: '#/definitions/schema', + }, + }, + additionalProperties: false, + }, + headerParameterSubSchema: { + additionalProperties: false, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + properties: { + required: { + type: 'boolean', + description: + 'Determines whether or not this parameter is required or optional.', + default: false, + }, + in: { + type: 'string', + description: 'Determines the location of the parameter.', + enum: ['header'], + }, + description: { + type: 'string', + description: + 'A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed.', + }, + name: { + type: 'string', + description: 'The name of the parameter.', + }, + type: { + type: 'string', + enum: ['string', 'number', 'boolean', 'integer', 'array'], + }, + format: { + type: 'string', + }, + items: { + $ref: '#/definitions/primitivesItems', + }, + collectionFormat: { + $ref: '#/definitions/collectionFormat', + }, + default: { + $ref: '#/definitions/default', + }, + maximum: { + $ref: '#/definitions/maximum', + }, + exclusiveMaximum: { + $ref: '#/definitions/exclusiveMaximum', + }, + minimum: { + $ref: '#/definitions/minimum', + }, + exclusiveMinimum: { + $ref: '#/definitions/exclusiveMinimum', + }, + maxLength: { + $ref: '#/definitions/maxLength', + }, + minLength: { + $ref: '#/definitions/minLength', + }, + pattern: { + $ref: '#/definitions/pattern', + }, + maxItems: { + $ref: '#/definitions/maxItems', + }, + minItems: { + $ref: '#/definitions/minItems', + }, + uniqueItems: { + $ref: '#/definitions/uniqueItems', + }, + enum: { + $ref: '#/definitions/enum', + }, + multipleOf: { + $ref: '#/definitions/multipleOf', + }, + }, + }, + queryParameterSubSchema: { + additionalProperties: false, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + properties: { + required: { + type: 'boolean', + description: + 'Determines whether or not this parameter is required or optional.', + default: false, + }, + in: { + type: 'string', + description: 'Determines the location of the parameter.', + enum: ['query'], + }, + description: { + type: 'string', + description: + 'A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed.', + }, + name: { + type: 'string', + description: 'The name of the parameter.', + }, + allowEmptyValue: { + type: 'boolean', + default: false, + description: + 'allows sending a parameter by name only or with an empty value.', + }, + type: { + type: 'string', + enum: ['string', 'number', 'boolean', 'integer', 'array'], + }, + format: { + type: 'string', + }, + items: { + $ref: '#/definitions/primitivesItems', + }, + collectionFormat: { + $ref: '#/definitions/collectionFormatWithMulti', + }, + default: { + $ref: '#/definitions/default', + }, + maximum: { + $ref: '#/definitions/maximum', + }, + exclusiveMaximum: { + $ref: '#/definitions/exclusiveMaximum', + }, + minimum: { + $ref: '#/definitions/minimum', + }, + exclusiveMinimum: { + $ref: '#/definitions/exclusiveMinimum', + }, + maxLength: { + $ref: '#/definitions/maxLength', + }, + minLength: { + $ref: '#/definitions/minLength', + }, + pattern: { + $ref: '#/definitions/pattern', + }, + maxItems: { + $ref: '#/definitions/maxItems', + }, + minItems: { + $ref: '#/definitions/minItems', + }, + uniqueItems: { + $ref: '#/definitions/uniqueItems', + }, + enum: { + $ref: '#/definitions/enum', + }, + multipleOf: { + $ref: '#/definitions/multipleOf', + }, + }, + }, + formDataParameterSubSchema: { + additionalProperties: false, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + properties: { + required: { + type: 'boolean', + description: + 'Determines whether or not this parameter is required or optional.', + default: false, + }, + in: { + type: 'string', + description: 'Determines the location of the parameter.', + enum: ['formData'], + }, + description: { + type: 'string', + description: + 'A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed.', + }, + name: { + type: 'string', + description: 'The name of the parameter.', + }, + allowEmptyValue: { + type: 'boolean', + default: false, + description: + 'allows sending a parameter by name only or with an empty value.', + }, + type: { + type: 'string', + enum: ['string', 'number', 'boolean', 'integer', 'array', 'file'], + }, + format: { + type: 'string', + }, + items: { + $ref: '#/definitions/primitivesItems', + }, + collectionFormat: { + $ref: '#/definitions/collectionFormatWithMulti', + }, + default: { + $ref: '#/definitions/default', + }, + maximum: { + $ref: '#/definitions/maximum', + }, + exclusiveMaximum: { + $ref: '#/definitions/exclusiveMaximum', + }, + minimum: { + $ref: '#/definitions/minimum', + }, + exclusiveMinimum: { + $ref: '#/definitions/exclusiveMinimum', + }, + maxLength: { + $ref: '#/definitions/maxLength', + }, + minLength: { + $ref: '#/definitions/minLength', + }, + pattern: { + $ref: '#/definitions/pattern', + }, + maxItems: { + $ref: '#/definitions/maxItems', + }, + minItems: { + $ref: '#/definitions/minItems', + }, + uniqueItems: { + $ref: '#/definitions/uniqueItems', + }, + enum: { + $ref: '#/definitions/enum', + }, + multipleOf: { + $ref: '#/definitions/multipleOf', + }, + }, + }, + pathParameterSubSchema: { + additionalProperties: false, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + required: ['required'], + properties: { + required: { + type: 'boolean', + enum: [true], + description: + 'Determines whether or not this parameter is required or optional.', + }, + in: { + type: 'string', + description: 'Determines the location of the parameter.', + enum: ['path'], + }, + description: { + type: 'string', + description: + 'A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed.', + }, + name: { + type: 'string', + description: 'The name of the parameter.', + }, + type: { + type: 'string', + enum: ['string', 'number', 'boolean', 'integer', 'array'], + }, + format: { + type: 'string', + }, + items: { + $ref: '#/definitions/primitivesItems', + }, + collectionFormat: { + $ref: '#/definitions/collectionFormat', + }, + default: { + $ref: '#/definitions/default', + }, + maximum: { + $ref: '#/definitions/maximum', + }, + exclusiveMaximum: { + $ref: '#/definitions/exclusiveMaximum', + }, + minimum: { + $ref: '#/definitions/minimum', + }, + exclusiveMinimum: { + $ref: '#/definitions/exclusiveMinimum', + }, + maxLength: { + $ref: '#/definitions/maxLength', + }, + minLength: { + $ref: '#/definitions/minLength', + }, + pattern: { + $ref: '#/definitions/pattern', + }, + maxItems: { + $ref: '#/definitions/maxItems', + }, + minItems: { + $ref: '#/definitions/minItems', + }, + uniqueItems: { + $ref: '#/definitions/uniqueItems', + }, + enum: { + $ref: '#/definitions/enum', + }, + multipleOf: { + $ref: '#/definitions/multipleOf', + }, + }, + }, + nonBodyParameter: { + type: 'object', + required: ['name', 'in', 'type'], + oneOf: [ + { + $ref: '#/definitions/headerParameterSubSchema', + }, + { + $ref: '#/definitions/formDataParameterSubSchema', + }, + { + $ref: '#/definitions/queryParameterSubSchema', + }, + { + $ref: '#/definitions/pathParameterSubSchema', + }, + ], + }, + parameter: { + oneOf: [ + { + $ref: '#/definitions/bodyParameter', + }, + { + $ref: '#/definitions/nonBodyParameter', + }, + ], + }, + schema: { + type: 'object', + description: 'A deterministic version of a JSON Schema object.', + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + properties: { + $ref: { + type: 'string', + }, + format: { + type: 'string', + }, + title: { + type: 'string', + }, + description: { + type: 'string', + }, + default: true, + multipleOf: { + type: 'number', + exclusiveMinimum: 0, + }, + maximum: { + type: 'number', + }, + exclusiveMaximum: { + type: 'number', + }, + minimum: { + type: 'number', + }, + exclusiveMinimum: { + type: 'number', + }, + maxLength: { + type: 'number', + minimum: 0, + }, + minLength: { + allOf: [{ type: 'number', minimum: 0 }, { default: 0 }], + }, + pattern: { + type: 'string', + format: 'regex', + }, + maxItems: { + type: 'number', + minimum: 0, + }, + minItems: { + allOf: [{ type: 'number', minimum: 0 }, { default: 0 }], + }, + uniqueItems: { + type: 'boolean', + default: false, + }, + maxProperties: { + type: 'number', + minimum: 0, + }, + minProperties: { + allOf: [{ type: 'number', minimum: 0 }, { default: 0 }], + }, + required: { + type: 'array', + items: { + type: 'string', + }, + uniqueItems: true, + default: [], + }, + enum: { + type: 'array', + items: true, + minItems: 1, + uniqueItems: true, + }, + additionalProperties: { + anyOf: [ + { + $ref: '#/definitions/schema', + }, + { + type: 'boolean', + }, + ], + default: {}, + }, + type: { + anyOf: [ + { + enum: [ + 'array', + 'boolean', + 'integer', + 'null', + 'number', + 'object', + 'string', + ], + }, + { + type: 'array', + items: { + enum: [ + 'array', + 'boolean', + 'integer', + 'null', + 'number', + 'object', + 'string', + ], + }, + minItems: 1, + uniqueItems: true, + }, + ], + }, + items: { + anyOf: [ + { + $ref: '#/definitions/schema', + }, + { + type: 'array', + minItems: 1, + items: { + $ref: '#/definitions/schema', + }, + }, + ], + default: {}, + }, + allOf: { + type: 'array', + minItems: 1, + items: { + $ref: '#/definitions/schema', + }, + }, + properties: { + type: 'object', + additionalProperties: { + $ref: '#/definitions/schema', + }, + default: {}, + }, + discriminator: { + type: 'string', + }, + readOnly: { + type: 'boolean', + default: false, + }, + xml: { + $ref: '#/definitions/xml', + }, + externalDocs: { + $ref: '#/definitions/externalDocs', + }, + example: {}, + }, + additionalProperties: false, + }, + fileSchema: { + type: 'object', + description: 'A deterministic version of a JSON Schema object.', + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + required: ['type'], + properties: { + format: { + type: 'string', + }, + title: { + type: 'string', + }, + description: { + type: 'string', + }, + default: true, + required: { + type: 'array', + items: { + type: 'string', + }, + uniqueItems: true, + default: [], + }, + type: { + type: 'string', + enum: ['file'], + }, + readOnly: { + type: 'boolean', + default: false, + }, + externalDocs: { + $ref: '#/definitions/externalDocs', + }, + example: {}, + }, + additionalProperties: false, + }, + primitivesItems: { + type: 'object', + additionalProperties: false, + properties: { + type: { + type: 'string', + enum: ['string', 'number', 'integer', 'boolean', 'array'], + }, + format: { + type: 'string', + }, + items: { + $ref: '#/definitions/primitivesItems', + }, + collectionFormat: { + $ref: '#/definitions/collectionFormat', + }, + default: { + $ref: '#/definitions/default', + }, + maximum: { + $ref: '#/definitions/maximum', + }, + exclusiveMaximum: { + $ref: '#/definitions/exclusiveMaximum', + }, + minimum: { + $ref: '#/definitions/minimum', + }, + exclusiveMinimum: { + $ref: '#/definitions/exclusiveMinimum', + }, + maxLength: { + $ref: '#/definitions/maxLength', + }, + minLength: { + $ref: '#/definitions/minLength', + }, + pattern: { + $ref: '#/definitions/pattern', + }, + maxItems: { + $ref: '#/definitions/maxItems', + }, + minItems: { + $ref: '#/definitions/minItems', + }, + uniqueItems: { + $ref: '#/definitions/uniqueItems', + }, + enum: { + $ref: '#/definitions/enum', + }, + multipleOf: { + $ref: '#/definitions/multipleOf', + }, + }, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + }, + security: { + type: 'array', + items: { + $ref: '#/definitions/securityRequirement', + }, + uniqueItems: true, + }, + securityRequirement: { + type: 'object', + additionalProperties: { + type: 'array', + items: { + type: 'string', + }, + uniqueItems: true, + }, + }, + xml: { + type: 'object', + additionalProperties: false, + properties: { + name: { + type: 'string', + }, + namespace: { + type: 'string', + }, + prefix: { + type: 'string', + }, + attribute: { + type: 'boolean', + default: false, + }, + wrapped: { + type: 'boolean', + default: false, + }, + }, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + }, + tag: { + type: 'object', + additionalProperties: false, + required: ['name'], + properties: { + name: { + type: 'string', + }, + description: { + type: 'string', + }, + externalDocs: { + $ref: '#/definitions/externalDocs', + }, + }, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + }, + securityDefinitions: { + type: 'object', + additionalProperties: { + oneOf: [ + { + $ref: '#/definitions/basicAuthenticationSecurity', + }, + { + $ref: '#/definitions/apiKeySecurity', + }, + { + $ref: '#/definitions/oauth2ImplicitSecurity', + }, + { + $ref: '#/definitions/oauth2PasswordSecurity', + }, + { + $ref: '#/definitions/oauth2ApplicationSecurity', + }, + { + $ref: '#/definitions/oauth2AccessCodeSecurity', + }, + ], + }, + }, + basicAuthenticationSecurity: { + type: 'object', + additionalProperties: false, + required: ['type'], + properties: { + type: { + type: 'string', + enum: ['basic'], + }, + description: { + type: 'string', + }, + }, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + }, + apiKeySecurity: { + type: 'object', + additionalProperties: false, + required: ['type', 'name', 'in'], + properties: { + type: { + type: 'string', + enum: ['apiKey'], + }, + name: { + type: 'string', + }, + in: { + type: 'string', + enum: ['header', 'query'], + }, + description: { + type: 'string', + }, + }, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + }, + oauth2ImplicitSecurity: { + type: 'object', + additionalProperties: false, + required: ['type', 'flow', 'authorizationUrl'], + properties: { + type: { + type: 'string', + enum: ['oauth2'], + }, + flow: { + type: 'string', + enum: ['implicit'], + }, + scopes: { + $ref: '#/definitions/oauth2Scopes', + }, + authorizationUrl: { + type: 'string', + format: 'uri', + }, + description: { + type: 'string', + }, + }, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + }, + oauth2PasswordSecurity: { + type: 'object', + additionalProperties: false, + required: ['type', 'flow', 'tokenUrl'], + properties: { + type: { + type: 'string', + enum: ['oauth2'], + }, + flow: { + type: 'string', + enum: ['password'], + }, + scopes: { + $ref: '#/definitions/oauth2Scopes', + }, + tokenUrl: { + type: 'string', + format: 'uri', + }, + description: { + type: 'string', + }, + }, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + }, + oauth2ApplicationSecurity: { + type: 'object', + additionalProperties: false, + required: ['type', 'flow', 'tokenUrl'], + properties: { + type: { + type: 'string', + enum: ['oauth2'], + }, + flow: { + type: 'string', + enum: ['application'], + }, + scopes: { + $ref: '#/definitions/oauth2Scopes', + }, + tokenUrl: { + type: 'string', + format: 'uri', + }, + description: { + type: 'string', + }, + }, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + }, + oauth2AccessCodeSecurity: { + type: 'object', + additionalProperties: false, + required: ['type', 'flow', 'authorizationUrl', 'tokenUrl'], + properties: { + type: { + type: 'string', + enum: ['oauth2'], + }, + flow: { + type: 'string', + enum: ['accessCode'], + }, + scopes: { + $ref: '#/definitions/oauth2Scopes', + }, + authorizationUrl: { + type: 'string', + format: 'uri', + }, + tokenUrl: { + type: 'string', + format: 'uri', + }, + description: { + type: 'string', + }, + }, + patternProperties: { + '^x-': { + $ref: '#/definitions/vendorExtension', + }, + }, + }, + oauth2Scopes: { + type: 'object', + additionalProperties: { + type: 'string', + }, + }, + mediaTypeList: { + type: 'array', + items: { + $ref: '#/definitions/mimeType', + }, + uniqueItems: true, + }, + parametersList: { + type: 'array', + description: 'The parameters needed to send a valid API call.', + additionalItems: false, + items: { + oneOf: [ + { + $ref: '#/definitions/parameter', + }, + { + $ref: '#/definitions/jsonReference', + }, + ], + }, + uniqueItems: true, + }, + schemesList: { + 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', + }, + title: { + type: 'string', + }, + description: { + type: 'string', + }, + default: true, + multipleOf: { + type: 'number', + exclusiveMinimum: 0, + }, + maximum: { + type: 'number', + }, + exclusiveMaximum: { + type: 'number', + }, + minimum: { + type: 'number', + }, + exclusiveMinimum: { + type: 'number', + }, + maxLength: { + type: 'number', + minimum: 0, + }, + minLength: { + allOf: [{ type: 'number', minimum: 0 }, { default: 0 }], + }, + pattern: { + type: 'string', + format: 'regex', + }, + maxItems: { + type: 'number', + minimum: 0, + }, + minItems: { + allOf: [{ type: 'number', minimum: 0 }, { default: 0 }], + }, + uniqueItems: { + type: 'boolean', + default: false, + }, + enum: { + type: 'array', + items: true, + minItems: 1, + uniqueItems: true, + }, + jsonReference: { + type: 'object', + required: ['$ref'], + additionalProperties: false, + properties: { + $ref: { + type: 'string', + }, + }, + }, + }, +}; + const openapi3_0_schema_object = { type: 'object', 'x-custom-validator': 'validateSchema', diff --git a/projects/openapi-io/src/validation/validator.test.ts b/projects/openapi-io/src/validation/validator.test.ts index 86d888d7ac..6cb5916f59 100644 --- a/projects/openapi-io/src/validation/validator.test.ts +++ b/projects/openapi-io/src/validation/validator.test.ts @@ -1,7 +1,11 @@ +import yaml from 'yaml'; import { test, expect, describe } from '@jest/globals'; import fs from 'node:fs/promises'; -import { validateOpenApiV3Document } from './validator'; +import { + validateSwaggerV2Document, + validateOpenApiV3Document, +} from './validator'; import { parseOpenAPIWithSourcemap } from '../parser/openapi-sourcemap-parser'; import path from 'path'; import { defaultEmptySpec } from '@useoptic/openapi-utilities'; @@ -11,239 +15,257 @@ async function readJson(p: string) { return JSON.parse(contents); } -describe('strict validation', () => { - test('valid open api document should not raise errors', async () => { - validateOpenApiV3Document(defaultEmptySpec); - validateOpenApiV3Document( - (await readJson('./inputs/openapi3/petstore0.json.flattened.json')) - .jsonLike - ); - }); +describe('2.x.x validation', () => { + describe('strict validation', () => { + test('valid swagger document should not raise errors', async () => { + const file = yaml.parse( + await fs.readFile('./inputs/swagger2/spec.yml', 'utf-8') + ); - test('valid open 3.1 api document should not raise errors', async () => { - validateOpenApiV3Document(defaultEmptySpec); - validateOpenApiV3Document( - await readJson('./inputs/openapi3/todo-api-3_1.json') - ); - }); + validateSwaggerV2Document(file); + }); - test('open api doc with no description in response should throw', () => { - expect(() => { - validateOpenApiV3Document( - { - openapi: '3.1.3', - info: { version: '0.0.0', title: 'Empty' }, + test('invalid swagger document should raise errors', async () => { + expect(() => { + validateSwaggerV2Document({ + swagger: '2.0', + info: { + title: '123', + version: '1.2.3', + }, + bad: '123', paths: { - '/example': { + '/api': { get: { - responses: { - '200': {}, - }, + responses: {}, }, }, }, - }, - undefined, - { strictOpenAPI: true } + }); + }).toThrowErrorMatchingSnapshot(); + }); + }); + + describe('non-strict validation', () => { + test('valid swagger document should not raise errors', async () => { + const file = yaml.parse( + await fs.readFile('./inputs/swagger2/spec.yml', 'utf-8') ); - }).toThrowErrorMatchingSnapshot(); + + validateSwaggerV2Document(file); + }); + + test('invalid swagger document should raise errors', async () => { + expect(() => { + validateSwaggerV2Document({ + swagger: '1', + info: { + title: '123', + }, + paths: { + '/api': { + get: {}, + }, + }, + }); + }).toThrowErrorMatchingSnapshot(); + }); }); +}); - test('advanced validators run and append their results', () => { - const json: any = { - ...defaultEmptySpec, - paths: { - '/api/users/{userId}': { - get: { - responses: { - '200': { - description: 'hello', - content: { - 'application/json': { - schema: { - type: 'object', - oneOf: [], - anyOf: [], - items: [], - }, +describe('3.x.x validation', () => { + describe('strict validation', () => { + test('valid open api document should not raise errors', async () => { + validateOpenApiV3Document(defaultEmptySpec); + validateOpenApiV3Document( + (await readJson('./inputs/openapi3/petstore0.json.flattened.json')) + .jsonLike + ); + }); + + test('valid open 3.1 api document should not raise errors', async () => { + validateOpenApiV3Document(defaultEmptySpec); + validateOpenApiV3Document( + await readJson('./inputs/openapi3/todo-api-3_1.json') + ); + }); + + test('open api doc with no description in response should throw', () => { + expect(() => { + validateOpenApiV3Document( + { + openapi: '3.1.3', + info: { version: '0.0.0', title: 'Empty' }, + paths: { + '/example': { + get: { + responses: { + '200': {}, }, }, }, }, }, - }, - }, - }; - - expect(() => { - validateOpenApiV3Document(json, undefined, { strictOpenAPI: true }); - }).toThrowErrorMatchingSnapshot(); - }); - - test('open api doc with extra custom parameters', () => { - validateOpenApiV3Document({ - ...defaultEmptySpec, - 'x-extra_property': { - abc: 'asd', - }, + undefined, + { strictOpenAPI: true } + ); + }).toThrowErrorMatchingSnapshot(); }); - validateOpenApiV3Document( - { + test('advanced validators run and append their results', () => { + const json: any = { ...defaultEmptySpec, paths: { - '/user/login': { + '/api/users/{userId}': { get: { - tags: ['user'], - 'x-maturity': 'wip', - summary: 'Logs user into the system', - operationId: 'loginUser', - parameters: [ - { - name: 'username', - in: 'query', - description: 'The user name for login', - required: true, - schema: { - type: 'string', - }, - }, - { - name: 'password', - in: 'query', - description: 'The password for login in clear text', - required: true, - schema: { - type: 'string', - }, - }, - ], responses: { '200': { - description: 'successful operation', - headers: { - 'X-Rate-Limit': { - description: 'calls per hour allowed by the user', - schema: { - type: 'integer', - format: 'int32', - }, - }, - 'X-Expires-After': { - description: 'date in UTC when token expires', - schema: { - type: 'string', - format: 'date-time', - }, - }, - }, + description: 'hello', content: { - 'application/xml': { - schema: { - type: 'string', - }, - }, 'application/json': { schema: { - type: 'string', + type: 'object', + oneOf: [], + anyOf: [], + items: [], }, }, }, }, - '400': { - description: 'Invalid username/password supplied', - content: {}, - }, }, }, }, }, - }, - undefined, - { strictOpenAPI: true } - ); - }); + }; - test('openapi with webhooks', async () => { - validateOpenApiV3Document( - await readJson('./inputs/openapi3/openapi-webhook.json') - ); - }); -}); + expect(() => { + validateOpenApiV3Document(json, undefined, { strictOpenAPI: true }); + }).toThrowErrorMatchingSnapshot(); + }); + + test('open api doc with extra custom parameters', () => { + validateOpenApiV3Document({ + ...defaultEmptySpec, + 'x-extra_property': { + abc: 'asd', + }, + }); -describe('non-strict validation', () => { - describe('should pass', () => { - test('open api doc with no description in response', () => { validateOpenApiV3Document( { - openapi: '3.1.3', - info: { version: '0.0.0', title: 'Empty' }, + ...defaultEmptySpec, paths: { - '/example': { + '/user/login': { get: { + tags: ['user'], + 'x-maturity': 'wip', + summary: 'Logs user into the system', + operationId: 'loginUser', + parameters: [ + { + name: 'username', + in: 'query', + description: 'The user name for login', + required: true, + schema: { + type: 'string', + }, + }, + { + name: 'password', + in: 'query', + description: 'The password for login in clear text', + required: true, + schema: { + type: 'string', + }, + }, + ], responses: { - '200': {}, + '200': { + description: 'successful operation', + headers: { + 'X-Rate-Limit': { + description: 'calls per hour allowed by the user', + schema: { + type: 'integer', + format: 'int32', + }, + }, + 'X-Expires-After': { + description: 'date in UTC when token expires', + schema: { + type: 'string', + format: 'date-time', + }, + }, + }, + content: { + 'application/xml': { + schema: { + type: 'string', + }, + }, + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + '400': { + description: 'Invalid username/password supplied', + content: {}, + }, }, }, }, }, }, undefined, - { strictOpenAPI: false } + { strictOpenAPI: true } ); }); - test('additional properties in invalid place', () => { + test('openapi with webhooks', async () => { validateOpenApiV3Document( - { - openapi: '3.1.3', - info: { version: '0.0.0', title: 'Empty', badproperty: ':(' }, - paths: { - '/example': { - get: { - responses: { - '200': {}, - }, - }, - }, - }, - }, - undefined, - { strictOpenAPI: false } + await readJson('./inputs/openapi3/openapi-webhook.json') ); }); }); - describe('should fail', () => { - test('open api doc without responses', () => { - expect(() => { + describe('non-strict validation', () => { + describe('should pass', () => { + test('open api doc with no description in response', () => { validateOpenApiV3Document( { openapi: '3.1.3', info: { version: '0.0.0', title: 'Empty' }, paths: { '/example': { - get: {}, + get: { + responses: { + '200': {}, + }, + }, }, }, }, undefined, { strictOpenAPI: false } ); - }).toThrowErrorMatchingSnapshot(); - }); + }); - test('open api doc with invalid status code shape', () => { - expect(() => { + test('additional properties in invalid place', () => { validateOpenApiV3Document( { openapi: '3.1.3', - info: { version: '0.0.0', title: 'Empty' }, + info: { version: '0.0.0', title: 'Empty', badproperty: ':(' }, paths: { '/example': { get: { responses: { - '202': null, + '200': {}, }, }, }, @@ -252,20 +274,62 @@ describe('non-strict validation', () => { undefined, { strictOpenAPI: false } ); - }).toThrowErrorMatchingSnapshot(); + }); }); - test('open api doc with no path should throw an error', () => { - expect(() => { - validateOpenApiV3Document( - { - openapi: '3.1.3', - info: { version: '0.0.0', title: 'Empty' }, - }, - undefined, - { strictOpenAPI: false } - ); - }).toThrowErrorMatchingSnapshot(); + describe('should fail', () => { + test('open api doc without responses', () => { + expect(() => { + validateOpenApiV3Document( + { + openapi: '3.1.3', + info: { version: '0.0.0', title: 'Empty' }, + paths: { + '/example': { + get: {}, + }, + }, + }, + undefined, + { strictOpenAPI: false } + ); + }).toThrowErrorMatchingSnapshot(); + }); + + test('open api doc with invalid status code shape', () => { + expect(() => { + validateOpenApiV3Document( + { + openapi: '3.1.3', + info: { version: '0.0.0', title: 'Empty' }, + paths: { + '/example': { + get: { + responses: { + '202': null, + }, + }, + }, + }, + }, + undefined, + { strictOpenAPI: false } + ); + }).toThrowErrorMatchingSnapshot(); + }); + + test('open api doc with no path should throw an error', () => { + expect(() => { + validateOpenApiV3Document( + { + openapi: '3.1.3', + info: { version: '0.0.0', title: 'Empty' }, + }, + undefined, + { strictOpenAPI: false } + ); + }).toThrowErrorMatchingSnapshot(); + }); }); }); }); diff --git a/projects/openapi-io/src/validation/validator.ts b/projects/openapi-io/src/validation/validator.ts index 46a32dfdd9..fdcfa72841 100644 --- a/projects/openapi-io/src/validation/validator.ts +++ b/projects/openapi-io/src/validation/validator.ts @@ -1,9 +1,11 @@ -import { OpenAPIV3, OpenAPI } from 'openapi-types'; +import { OpenAPI } from 'openapi-types'; import ajv, { ErrorObject, ValidateFunction } from 'ajv'; import addFormats from 'ajv-formats'; import { + swagger2_schema_object, + basic_swagger2_object, openapi3_1_json_schema, openapi3_0_json_schema, basic3openapi_schema, @@ -16,6 +18,7 @@ import { JsonSchemaSourcemap } from '../parser/sourcemap'; import { attachAdvancedValidators } from './advanced-validation'; import { ValidationError } from './errors'; import { jsonPointerHelpers } from '@useoptic/json-pointer-helpers'; +import { FlatOpenAPIV2, FlatOpenAPIV3 } from '@useoptic/openapi-utilities'; type Options = { strictOpenAPI: boolean; @@ -24,6 +27,29 @@ type Options = { export default class OpenAPISchemaValidator { constructor(private options: Options) {} + public validate2(openapiDoc: OpenAPI.Document): { + errors: ErrorObject[]; + } { + const ajvInstance = new ajv({ allErrors: true, strict: false }); + ajvErrors(ajvInstance); + addFormats(ajvInstance); + let validator: ValidateFunction; + if (this.options.strictOpenAPI) { + attachAdvancedValidators(ajvInstance); + ajvInstance.addSchema(swagger2_schema_object); + validator = ajvInstance.compile(swagger2_schema_object); + } else { + ajvInstance.addSchema(basic_swagger2_object); + validator = ajvInstance.compile(basic_swagger2_object); + } + + if (!validator(openapiDoc) && validator.errors) { + return { errors: validator.errors }; + } else { + return { errors: [] }; + } + } + public validate3_0(openapiDoc: OpenAPI.Document): { errors: ErrorObject[]; } { @@ -126,11 +152,34 @@ export const processValidatorErrors = ( .filter((value, index, array) => array.indexOf(value) === index); }; +export const validateSwaggerV2Document = ( + spec: any, + sourcemap?: JsonSchemaSourcemap, + validatorOptions: Options = { strictOpenAPI: true } +): FlatOpenAPIV2.Document => { + const validator = new OpenAPISchemaValidator(validatorOptions); + + let results = validator.validate2(spec); + // will throw for unsupported spec version before running + + if (results && results.errors.length > 0) { + const processedErrors = processValidatorErrors( + spec, + results.errors, + sourcemap + ); + + throw new ValidationError(processedErrors.join('\n')); + } + + return spec as FlatOpenAPIV2.Document; +}; + export const validateOpenApiV3Document = ( spec: any, sourcemap?: JsonSchemaSourcemap, validatorOptions: Options = { strictOpenAPI: true } -): OpenAPIV3.Document => { +): FlatOpenAPIV3.Document => { const validator = new OpenAPISchemaValidator(validatorOptions); let results: @@ -154,5 +203,5 @@ export const validateOpenApiV3Document = ( throw new ValidationError(processedErrors.join('\n')); } - return spec as OpenAPIV3.Document; + return spec as FlatOpenAPIV3.Document; }; diff --git a/projects/optic/src/utils/spec-loaders.ts b/projects/optic/src/utils/spec-loaders.ts index 71f3e1c723..95deb4c4e3 100644 --- a/projects/optic/src/utils/spec-loaders.ts +++ b/projects/optic/src/utils/spec-loaders.ts @@ -11,6 +11,7 @@ import { OpenAPIV3, } from '@useoptic/openapi-utilities'; import { + validateSwaggerV2Document, validateOpenApiV3Document, filePathToGitPath, parseOpenAPIFromRepoWithSourcemap, @@ -316,7 +317,9 @@ function validateAndDenormalize( } ): ParseResult { if (parseResult.version === '2.x.x') { - // TODO + validateSwaggerV2Document(parseResult.jsonLike, parseResult.sourcemap, { + strictOpenAPI: options.strict, + }); } else if ( parseResult.version === '3.0.x' || parseResult.version === '3.1.x'