From 79f5ed83e3881dd112bb0d544159c9e9ec3f856d Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Thu, 12 Aug 2021 16:19:15 +0200 Subject: [PATCH] p1c2u/openapi-core#296: Implements OpenAPI 3.1 spec validator --- .github/workflows/python-test.yml | 2 +- openapi_spec_validator/__init__.py | 37 +- openapi_spec_validator/factories.py | 33 +- openapi_spec_validator/readers.py | 5 +- .../resources/schemas/v3.1/schema.json | 1347 +++++++++++++++++ openapi_spec_validator/validators.py | 8 +- requirements.txt | 2 +- setup.cfg | 8 +- tests/integration/conftest.py | 8 +- tests/integration/test_validate.py | 49 + tests/integration/test_validators.py | 6 +- 11 files changed, 1476 insertions(+), 29 deletions(-) create mode 100644 openapi_spec_validator/resources/schemas/v3.1/schema.json diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 4a36162..a46fbe9 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -13,7 +13,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9] + python-version: [3.7, 3.8, 3.9] os: [windows-latest, ubuntu-latest] fail-fast: false steps: diff --git a/openapi_spec_validator/__init__.py b/openapi_spec_validator/__init__.py index 0238ca5..0b2250b 100644 --- a/openapi_spec_validator/__init__.py +++ b/openapi_spec_validator/__init__.py @@ -4,7 +4,8 @@ ) from openapi_spec_validator.handlers import UrlHandler, FileObjectHandler from openapi_spec_validator.schemas import get_openapi_schema -from openapi_spec_validator.factories import JSONSpecValidatorFactory +from openapi_spec_validator.factories import \ + Draft202012JSONSpecValidatorFactory, Draft4JSONSpecValidatorFactory from openapi_spec_validator.validators import SpecValidator __author__ = 'Artur Maciag' @@ -14,9 +15,15 @@ __license__ = 'Apache License, Version 2.0' __all__ = [ - 'openapi_v2_spec_validator', 'openapi_v3_spec_validator', - 'validate_v2_spec', 'validate_v3_spec', 'validate_spec', - 'validate_v2_spec_url', 'validate_v3_spec_url', 'validate_spec_url', + 'openapi_v2_spec_validator', + 'openapi_v3_spec_validator', + 'openapi_v31_spec_validator', + 'validate_v2_spec', + 'validate_v3_spec', + 'validate_spec', + 'validate_v2_spec_url', + 'validate_v3_spec_url', + 'validate_spec_url', ] file_object_handler = FileObjectHandler() @@ -30,7 +37,7 @@ # v2.0 spec schema_v2, schema_v2_url = get_openapi_schema('2.0') -openapi_v2_validator_factory = JSONSpecValidatorFactory( +openapi_v2_validator_factory = Draft4JSONSpecValidatorFactory( schema_v2, schema_v2_url, resolver_handlers=default_handlers, ) @@ -41,7 +48,7 @@ # v3.0 spec schema_v3, schema_v3_url = get_openapi_schema('3.0') -openapi_v3_validator_factory = JSONSpecValidatorFactory( +openapi_v3_validator_factory = Draft4JSONSpecValidatorFactory( schema_v3, schema_v3_url, resolver_handlers=default_handlers, ) @@ -50,6 +57,18 @@ resolver_handlers=default_handlers, ) +# v3.1 spec +schema_v31, schema_v31_url = get_openapi_schema('3.1') +openapi_v31_validator_factory = Draft202012JSONSpecValidatorFactory( + schema_v31, schema_v31_url, + resolver_handlers=default_handlers, +) +openapi_v31_spec_validator = SpecValidator( + openapi_v31_validator_factory, + resolver_handlers=default_handlers, +) + + # shortcuts validate_v2_spec = validate_spec_factory(openapi_v2_spec_validator.validate) validate_v2_spec_url = validate_spec_url_factory( @@ -59,6 +78,12 @@ validate_v3_spec_url = validate_spec_url_factory( openapi_v3_spec_validator.validate, default_handlers) + +validate_v31_spec = validate_spec_factory(openapi_v31_spec_validator.validate) +validate_v31_spec_url = validate_spec_url_factory( + openapi_v31_spec_validator.validate, default_handlers) + + # aliases to the latest version validate_spec = validate_v3_spec validate_spec_url = validate_v3_spec_url diff --git a/openapi_spec_validator/factories.py b/openapi_spec_validator/factories.py index 956235a..5baf723 100644 --- a/openapi_spec_validator/factories.py +++ b/openapi_spec_validator/factories.py @@ -1,6 +1,7 @@ """OpenAPI spec validator factories module.""" from jsonschema import validators -from jsonschema.validators import Draft4Validator, RefResolver +from jsonschema.validators import Draft4Validator, RefResolver, \ + Draft202012Validator from openapi_spec_validator.generators import ( SpecValidatorsGeneratorFactory, @@ -28,7 +29,7 @@ def _get_spec_validators(cls, spec_resolver): return dict(list(generator)) -class JSONSpecValidatorFactory: +class BaseJSONSpecValidatorFactory: """ Json documents validator factory against a json schema. @@ -36,8 +37,7 @@ class JSONSpecValidatorFactory: :param schema_url: schema base uri. """ - schema_validator_class = Draft4Validator - spec_validator_factory = Draft4ExtendedValidatorFactory + schema_validator_class = None def __init__(self, schema, schema_url='', resolver_handlers=None): self.schema = schema @@ -46,6 +46,17 @@ def __init__(self, schema, schema_url='', resolver_handlers=None): self.schema_validator_class.check_schema(self.schema) + +class Draft4JSONSpecValidatorFactory(BaseJSONSpecValidatorFactory): + """ + Json documents validator factory that uses Draft4Validator + + :param schema: schema for validation. + :param schema_url: schema base uri. + """ + schema_validator_class = Draft4Validator + spec_validator_factory = Draft4ExtendedValidatorFactory + @property def schema_resolver(self): return self._get_resolver(self.schema_url, self.schema) @@ -67,3 +78,17 @@ def create(self, spec_resolver): def _get_resolver(self, base_uri, referrer): return RefResolver( base_uri, referrer, handlers=self.resolver_handlers) + + +class Draft202012JSONSpecValidatorFactory(BaseJSONSpecValidatorFactory): + """ + Json documents validator factory that uses Draft202012Validator + + :param schema: schema for validation. + :param schema_url: schema base uri. + """ + + schema_validator_class = Draft202012Validator + + def create(self, spec_resolver): + return self.schema_validator_class(self.schema) diff --git a/openapi_spec_validator/readers.py b/openapi_spec_validator/readers.py index 5fb5f80..7c4fe6c 100644 --- a/openapi_spec_validator/readers.py +++ b/openapi_spec_validator/readers.py @@ -1,8 +1,5 @@ import os -try: - import pathlib -except ImportError: - import pathlib2 as pathlib +import pathlib import sys from openapi_spec_validator import all_urls_handler, file_object_handler diff --git a/openapi_spec_validator/resources/schemas/v3.1/schema.json b/openapi_spec_validator/resources/schemas/v3.1/schema.json new file mode 100644 index 0000000..44fdbb9 --- /dev/null +++ b/openapi_spec_validator/resources/schemas/v3.1/schema.json @@ -0,0 +1,1347 @@ +{ + "$id": "https://spec.openapis.org/oas/3.1/schema/2021-05-20", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "openapi": { + "type": "string", + "pattern": "^3\\.1\\.\\d+(-.+)?$" + }, + "info": { + "$ref": "#/$defs/info" + }, + "jsonSchemaDialect": { + "type": "string", + "format": "uri", + "default": "https://spec.openapis.org/oas/3.1/dialect/base" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/server" + } + }, + "paths": { + "$ref": "#/$defs/paths" + }, + "webhooks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/path-item-or-reference" + } + }, + "components": { + "$ref": "#/$defs/components" + }, + "security": { + "type": "array", + "items": { + "$ref": "#/$defs/security-requirement" + } + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/$defs/tag" + } + }, + "externalDocs": { + "$ref": "#/$defs/external-documentation" + } + }, + "required": [ + "openapi", + "info" + ], + "anyOf": [ + { + "required": [ + "paths" + ] + }, + { + "required": [ + "components" + ] + }, + { + "required": [ + "webhooks" + ] + } + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false, + "$defs": { + "info": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "termsOfService": { + "type": "string" + }, + "contact": { + "$ref": "#/$defs/contact" + }, + "license": { + "$ref": "#/$defs/license" + }, + "version": { + "type": "string" + } + }, + "required": [ + "title", + "version" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "contact": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "email": { + "type": "string" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "license": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "identifier" + ] + }, + { + "required": [ + "url" + ] + } + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "server": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri-reference" + }, + "description": { + "type": "string" + }, + "variables": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/server-variable" + } + } + }, + "required": [ + "url" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "server-variable": { + "type": "object", + "properties": { + "enum": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "default": { + "type": "string" + }, + "descriptions": { + "type": "string" + } + }, + "required": [ + "default" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "components": { + "type": "object", + "properties": { + "schemas": { + "type": "object", + "additionalProperties": { + "$dynamicRef": "#meta" + } + }, + "responses": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/response-or-reference" + } + }, + "parameters": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/parameter-or-reference" + } + }, + "examples": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/example-or-reference" + } + }, + "requestBodies": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/request-body-or-reference" + } + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/header-or-reference" + } + }, + "securitySchemes": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/security-scheme-or-reference" + } + }, + "links": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/link-or-reference" + } + }, + "callbacks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/callbacks-or-reference" + } + }, + "pathItems": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/path-item-or-reference" + } + } + }, + "patternProperties": { + "^(schemas|responses|parameters|examples|requestBodies|headers|securitySchemes|links|callbacks|pathItems)$": { + "$comment": "Enumerating all of the property names in the regex above is necessary for unevaluatedProperties to work as expected", + "propertyNames": { + "pattern": "^[a-zA-Z0-9._-]+$" + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "paths": { + "type": "object", + "patternProperties": { + "^/": { + "$ref": "#/$defs/path-item" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "path-item": { + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/server" + } + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/$defs/parameter-or-reference" + } + } + }, + "patternProperties": { + "^(get|put|post|delete|options|head|patch|trace)$": { + "$ref": "#/$defs/operation" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "path-item-or-reference": { + "if": { + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/path-item" + } + }, + "operation": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/$defs/external-documentation" + }, + "operationId": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/$defs/parameter-or-reference" + } + }, + "requestBody": { + "$ref": "#/$defs/request-body-or-reference" + }, + "responses": { + "$ref": "#/$defs/responses" + }, + "callbacks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/callbacks-or-reference" + } + }, + "deprecated": { + "default": false, + "type": "boolean" + }, + "security": { + "type": "array", + "items": { + "$ref": "#/$defs/security-requirement" + } + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/server" + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "external-documentation": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "url" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "parameter": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "in": { + "enum": [ + "query", + "header", + "path", + "cookie" + ] + }, + "description": { + "type": "string" + }, + "required": { + "default": false, + "type": "boolean" + }, + "deprecated": { + "default": false, + "type": "boolean" + }, + "allowEmptyValue": { + "default": false, + "type": "boolean" + }, + "schema": { + "$dynamicRef": "#meta" + }, + "content": { + "$ref": "#/$defs/content" + } + }, + "required": [ + "in" + ], + "oneOf": [ + { + "required": [ + "schema" + ] + }, + { + "required": [ + "content" + ] + } + ], + "dependentSchemas": { + "schema": { + "properties": { + "style": { + "type": "string" + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "default": false, + "type": "boolean" + } + }, + "allOf": [ + { + "$ref": "#/$defs/examples" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-path" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-header" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-query" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-cookie" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-form" + } + ], + "$defs": { + "styles-for-path": { + "if": { + "properties": { + "in": { + "const": "path" + } + }, + "required": [ + "in" + ] + }, + "then": { + "properties": { + "style": { + "default": "simple", + "enum": [ + "matrix", + "label", + "simple" + ] + }, + "required": { + "const": true + } + }, + "required": [ + "required" + ] + } + }, + "styles-for-header": { + "if": { + "properties": { + "in": { + "const": "header" + } + }, + "required": [ + "in" + ] + }, + "then": { + "properties": { + "style": { + "default": "simple", + "enum": [ + "simple" + ] + } + } + } + }, + "styles-for-query": { + "if": { + "properties": { + "in": { + "const": "query" + } + }, + "required": [ + "in" + ] + }, + "then": { + "properties": { + "style": { + "default": "form", + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + } + } + } + }, + "styles-for-cookie": { + "if": { + "properties": { + "in": { + "const": "cookie" + } + }, + "required": [ + "in" + ] + }, + "then": { + "properties": { + "style": { + "default": "form", + "enum": [ + "form" + ] + } + } + } + }, + "styles-for-form": { + "if": { + "properties": { + "style": { + "const": "form" + } + }, + "required": [ + "style" + ] + }, + "then": { + "properties": { + "explode": { + "default": true + } + } + }, + "else": { + "properties": { + "explode": { + "default": false + } + } + } + } + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "parameter-or-reference": { + "if": { + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/parameter" + } + }, + "request-body": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "content": { + "$ref": "#/$defs/content" + }, + "required": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "content" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "request-body-or-reference": { + "if": { + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/request-body" + } + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/media-type" + }, + "propertyNames": { + "format": "media-range" + } + }, + "media-type": { + "type": "object", + "properties": { + "schema": { + "$dynamicRef": "#meta" + }, + "encoding": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/encoding" + } + } + }, + "allOf": [ + { + "$ref": "#/$defs/specification-extensions" + }, + { + "$ref": "#/$defs/examples" + } + ], + "unevaluatedProperties": false + }, + "encoding": { + "type": "object", + "properties": { + "contentType": { + "type": "string", + "format": "media-range" + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/header-or-reference" + } + }, + "style": { + "default": "form", + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "default": false, + "type": "boolean" + } + }, + "allOf": [ + { + "$ref": "#/$defs/specification-extensions" + }, + { + "$ref": "#/$defs/encoding/$defs/explode-default" + } + ], + "unevaluatedProperties": false, + "$defs": { + "explode-default": { + "if": { + "properties": { + "style": { + "const": "form" + } + }, + "required": [ + "style" + ] + }, + "then": { + "properties": { + "explode": { + "default": true + } + } + }, + "else": { + "properties": { + "explode": { + "default": false + } + } + } + } + } + }, + "responses": { + "type": "object", + "properties": { + "default": { + "$ref": "#/$defs/response-or-reference" + } + }, + "patternProperties": { + "^[1-5][0-9X]{2}$": { + "$ref": "#/$defs/response-or-reference" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "response": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/header-or-reference" + } + }, + "content": { + "$ref": "#/$defs/content" + }, + "links": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/link-or-reference" + } + } + }, + "required": [ + "description" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "response-or-reference": { + "if": { + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/response" + } + }, + "callbacks": { + "type": "object", + "$ref": "#/$defs/specification-extensions", + "additionalProperties": { + "$ref": "#/$defs/path-item-or-reference" + } + }, + "callbacks-or-reference": { + "if": { + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/callbacks" + } + }, + "example": { + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "value": true, + "externalValue": { + "type": "string", + "format": "uri" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "example-or-reference": { + "if": { + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/example" + } + }, + "link": { + "type": "object", + "properties": { + "operationRef": { + "type": "string", + "format": "uri-reference" + }, + "operationId": true, + "parameters": { + "$ref": "#/$defs/map-of-strings" + }, + "requestBody": true, + "description": { + "type": "string" + }, + "body": { + "$ref": "#/$defs/server" + } + }, + "oneOf": [ + { + "required": [ + "operationRef" + ] + }, + { + "required": [ + "operationId" + ] + } + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "link-or-reference": { + "if": { + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/link" + } + }, + "header": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "required": { + "default": false, + "type": "boolean" + }, + "deprecated": { + "default": false, + "type": "boolean" + }, + "allowEmptyValue": { + "default": false, + "type": "boolean" + } + }, + "dependentSchemas": { + "schema": { + "properties": { + "style": { + "default": "simple", + "enum": [ + "simple" + ] + }, + "explode": { + "default": false, + "type": "boolean" + }, + "allowReserved": { + "default": false, + "type": "boolean" + }, + "schema": { + "$dynamicRef": "#meta" + } + }, + "$ref": "#/$defs/examples" + }, + "content": { + "properties": { + "content": { + "$ref": "#/$defs/content" + } + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "header-or-reference": { + "if": { + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/header" + } + }, + "tag": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/$defs/external-documentation" + } + }, + "required": [ + "name" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "reference": { + "type": "object", + "properties": { + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "unevaluatedProperties": false + }, + "schema": { + "$dynamicAnchor": "meta", + "type": [ + "object", + "boolean" + ] + }, + "security-scheme": { + "type": "object", + "properties": { + "type": { + "enum": [ + "apiKey", + "http", + "mutualTLS", + "oauth2", + "openIdConnect" + ] + }, + "description": { + "type": "string" + } + }, + "required": [ + "type" + ], + "allOf": [ + { + "$ref": "#/$defs/specification-extensions" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-apikey" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-http" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-http-bearer" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-oauth2" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-oidc" + } + ], + "unevaluatedProperties": false, + "$defs": { + "type-apikey": { + "if": { + "properties": { + "type": { + "const": "apiKey" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "name": { + "type": "string" + }, + "in": { + "enum": [ + "query", + "header", + "cookie" + ] + } + }, + "required": [ + "name", + "in" + ] + } + }, + "type-http": { + "if": { + "properties": { + "type": { + "const": "http" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "scheme": { + "type": "string" + } + }, + "required": [ + "scheme" + ] + } + }, + "type-http-bearer": { + "if": { + "properties": { + "type": { + "const": "http" + }, + "scheme": { + "const": "bearer" + } + }, + "required": [ + "type", + "scheme" + ] + }, + "then": { + "properties": { + "bearerFormat": { + "type": "string" + } + }, + "required": [ + "scheme" + ] + } + }, + "type-oauth2": { + "if": { + "properties": { + "type": { + "const": "oauth2" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "flows": { + "$ref": "#/$defs/oauth-flows" + } + }, + "required": [ + "flows" + ] + } + }, + "type-oidc": { + "if": { + "properties": { + "type": { + "const": "openIdConnect" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "openIdConnectUrl": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "openIdConnectUrl" + ] + } + } + } + }, + "security-scheme-or-reference": { + "if": { + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/security-scheme" + } + }, + "oauth-flows": { + "type": "object", + "properties": { + "implicit": { + "$ref": "#/$defs/oauth-flows/$defs/implicit" + }, + "password": { + "$ref": "#/$defs/oauth-flows/$defs/password" + }, + "clientCredentials": { + "$ref": "#/$defs/oauth-flows/$defs/client-credentials" + }, + "authorizationCode": { + "$ref": "#/$defs/oauth-flows/$defs/authorization-code" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false, + "$defs": { + "implicit": { + "type": "object", + "properties": { + "authorizationUrl": { + "type": "string" + }, + "refreshUrl": { + "type": "string" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "authorizationUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "password": { + "type": "object", + "properties": { + "tokenUrl": { + "type": "string" + }, + "refreshUrl": { + "type": "string" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "client-credentials": { + "type": "object", + "properties": { + "tokenUrl": { + "type": "string" + }, + "refreshUrl": { + "type": "string" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "authorization-code": { + "type": "object", + "properties": { + "authorizationUrl": { + "type": "string" + }, + "tokenUrl": { + "type": "string" + }, + "refreshUrl": { + "type": "string" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "authorizationUrl", + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + } + } + }, + "security-requirement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "specification-extensions": { + "patternProperties": { + "^x-": true + } + }, + "examples": { + "properties": { + "example": true, + "examples": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/example-or-reference" + } + } + } + }, + "map-of-strings": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } +} diff --git a/openapi_spec_validator/validators.py b/openapi_spec_validator/validators.py index ee26961..49b50b1 100644 --- a/openapi_spec_validator/validators.py +++ b/openapi_spec_validator/validators.py @@ -2,7 +2,7 @@ import string from jsonschema.validators import RefResolver -from openapi_schema_validator import OAS30Validator, oas30_format_checker +from openapi_schema_validator import OAS31Validator, oas31_format_checker from six import iteritems from openapi_spec_validator.exceptions import ( @@ -124,6 +124,8 @@ def __init__(self, dereferencer): @wraps_errors def iter_errors(self, schema, require_properties=True): schema_deref = self.dereferencer.dereference(schema) + if not isinstance(schema_deref, dict): + return if 'allOf' in schema_deref: for inner_schema in schema_deref['allOf']: @@ -339,10 +341,10 @@ def __init__(self, dereferencer): @wraps_errors def iter_errors(self, schema, value): - validator = OAS30Validator( + validator = OAS31Validator( schema, resolver=self.dereferencer.resolver_manager.resolver, - format_checker=oas30_format_checker, + format_checker=oas31_format_checker, ) for err in validator.iter_errors(value): yield err diff --git a/requirements.txt b/requirements.txt index dac7189..48184d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ jsonschema -openapi-schema-validator +git+https://github.com/anexia-it/openapi-schema-validator@master PyYAML==5.3.1 six==1.12.0 requests diff --git a/setup.cfg b/setup.cfg index 62422e1..cd3910c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,8 +9,6 @@ classifiers = Intended Audience :: Developers Topic :: Software Development :: Libraries :: Python Modules Operating System :: OS Independent - Programming Language :: Python :: 2.7 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 @@ -21,17 +19,15 @@ include_package_data = True packages = find: zip_safe = False test_suite = tests -python_requires = >= 2.7, != 3.0.*, != 3.1.*, != 3.2.*, != 3.3.*, != 3.4.* +python_requires = >= 3.6 setup_requires = setuptools install_requires = jsonschema - openapi-schema-validator + openapi-schema-validator @ git+https://github.com/anexia-it/openapi-schema-validator@master PyYAML>=5.1 six - pathlib2; python_version<"3.0" tests_require = - mock; python_version<"3.0" pytest pytest-flake8 pytest-cov diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index a83ced1..57e1792 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -6,7 +6,8 @@ from yaml import safe_load from openapi_spec_validator import (openapi_v3_spec_validator, - openapi_v2_spec_validator) + openapi_v2_spec_validator, + openapi_v31_spec_validator) from openapi_spec_validator.schemas import read_yaml_file @@ -46,6 +47,11 @@ def validator(): return openapi_v3_spec_validator +@pytest.fixture +def validator_v31(): + return openapi_v31_spec_validator + + @pytest.fixture def swagger_validator(): return openapi_v2_spec_validator diff --git a/tests/integration/test_validate.py b/tests/integration/test_validate.py index 9bb3e08..b75424a 100644 --- a/tests/integration/test_validate.py +++ b/tests/integration/test_validate.py @@ -2,6 +2,14 @@ from openapi_spec_validator.exceptions import OpenAPIValidationError +REMOTE_SOURCE_URL = 'https://raw.githubusercontent.com/' \ + 'OAI/OpenAPI-Specification/' \ + 'd9ac75b00c8bf405c2c90cfa9f20370564371dec/' + + +def remote_test_suite_file_path(test_file): + return "{}{}".format(REMOTE_SOURCE_URL, test_file) + class BaseTestValidOpeAPIv3Validator(object): @@ -98,3 +106,44 @@ def spec(self, factory): 'api-with-examples.yaml' ) return factory.spec_from_url(url) + + +class TestValidOpeAPIv31Validator(BaseTestValidOpeAPIv3Validator): + + @pytest.mark.parametrize('spec_file', [ + 'comp_pathitems.yaml', + 'info_summary.yaml', + 'license_identifier.yaml', + 'mega.yaml', + 'minimal_comp.yaml', + 'minimal_hooks.yaml', + 'minimal_paths.yaml', + 'path_no_response.yaml', + 'path_var_empty_pathitem.yaml', + 'schema.yaml', + 'servers.yaml', + 'valid_schema_types.yaml', + ]) + def test_valid(self, factory, validator_v31, spec_file, spec_url): + url = remote_test_suite_file_path( + '{}{}'.format('tests/v3.1/pass/', spec_file) + ) + spec = factory.spec_from_url(url) + + return validator_v31.validate(spec, spec_url=spec_url) + + @pytest.mark.parametrize('spec_file', [ + 'invalid_schema_types.yaml', + 'no_containers.yaml', + 'server_enum_empty.yaml', + 'servers.yaml', + 'unknown_container.yaml', + ]) + def test_failed(self, factory, validator_v31, spec_file, spec_url): + url = remote_test_suite_file_path( + '{}{}'.format('tests/v3.1/fail/', spec_file) + ) + spec = factory.spec_from_url(url) + + with pytest.raises(OpenAPIValidationError): + validator_v31.validate(spec, spec_url=spec_url) diff --git a/tests/integration/test_validators.py b/tests/integration/test_validators.py index 5172b88..d2f7c54 100644 --- a/tests/integration/test_validators.py +++ b/tests/integration/test_validators.py @@ -253,7 +253,7 @@ def test_default_value_wrong_type(self, validator): assert len(errors_list) == 1 assert errors_list[0].__class__ == OpenAPIValidationError assert errors_list[0].message == ( - "'invaldtype' is not of type integer" + "'invaldtype' is not of type 'integer'" ) def test_parameter_default_value_wrong_type(self, validator): @@ -293,7 +293,7 @@ def test_parameter_default_value_wrong_type(self, validator): assert len(errors_list) == 1 assert errors_list[0].__class__ == OpenAPIValidationError assert errors_list[0].message == ( - "'invaldtype' is not of type integer" + "'invaldtype' is not of type 'integer'" ) def test_parameter_default_value_wrong_type_swagger(self, @@ -332,7 +332,7 @@ def test_parameter_default_value_wrong_type_swagger(self, assert len(errors_list) == 1 assert errors_list[0].__class__ == OpenAPIValidationError assert errors_list[0].message == ( - "'invaldtype' is not of type integer" + "'invaldtype' is not of type 'integer'" ) def test_parameter_default_value_with_reference(self, validator):