diff --git a/openapi_core/schema/schemas/enums.py b/openapi_core/schema/schemas/enums.py index 3ba8b88..648ad5b 100644 --- a/openapi_core/schema/schemas/enums.py +++ b/openapi_core/schema/schemas/enums.py @@ -4,6 +4,7 @@ from enum import Enum class SchemaType(Enum): + ANY = None INTEGER = 'integer' NUMBER = 'number' STRING = 'string' diff --git a/openapi_core/schema/schemas/exceptions.py b/openapi_core/schema/schemas/exceptions.py index 3222a39..ef51d21 100644 --- a/openapi_core/schema/schemas/exceptions.py +++ b/openapi_core/schema/schemas/exceptions.py @@ -5,6 +5,14 @@ class OpenAPISchemaError(OpenAPIMappingError): pass +class NoValidSchema(OpenAPISchemaError): + pass + + +class UndefinedItemsSchema(OpenAPISchemaError): + pass + + class InvalidSchemaValue(OpenAPISchemaError): pass diff --git a/openapi_core/schema/schemas/factories.py b/openapi_core/schema/schemas/factories.py index 4d1f07f..e770c0e 100644 --- a/openapi_core/schema/schemas/factories.py +++ b/openapi_core/schema/schemas/factories.py @@ -16,7 +16,7 @@ class SchemaFactory(object): def create(self, schema_spec): schema_deref = self.dereferencer.dereference(schema_spec) - schema_type = schema_deref.get('type', 'object') + schema_type = schema_deref.get('type', None) schema_format = schema_deref.get('format') model = schema_deref.get('x-model', None) required = schema_deref.get('required', False) diff --git a/openapi_core/schema/schemas/models.py b/openapi_core/schema/schemas/models.py index f62b3f9..2aabf45 100644 --- a/openapi_core/schema/schemas/models.py +++ b/openapi_core/schema/schemas/models.py @@ -10,7 +10,8 @@ from openapi_core.extensions.models.factories import ModelFactory from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType from openapi_core.schema.schemas.exceptions import ( InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty, - OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema, + OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema, NoValidSchema, + UndefinedItemsSchema, ) from openapi_core.schema.schemas.util import ( forcebool, format_date, format_datetime, @@ -46,7 +47,7 @@ class Schema(object): } TYPE_VALIDATOR_CALLABLE_GETTER = { - None: lambda x: True, + SchemaType.ANY: lambda x: x, SchemaType.BOOLEAN: TypeValidator(bool), SchemaType.INTEGER: TypeValidator(integer_types, exclude=bool), SchemaType.NUMBER: TypeValidator(integer_types, float, exclude=bool), @@ -61,7 +62,7 @@ class Schema(object): schema_format=None, required=None, default=None, nullable=False, enum=None, deprecated=False, all_of=None, one_of=None, additional_properties=None): - self.type = schema_type and SchemaType(schema_type) + self.type = SchemaType(schema_type) self.model = model self.properties = properties and dict(properties) or {} self.items = items @@ -107,7 +108,7 @@ class Schema(object): return dict( (prop_name, val) - for prop_name, val in all_properties.items() + for prop_name, val in iteritems(all_properties) if prop_name in required ) @@ -124,6 +125,7 @@ class Schema(object): mapping = self.DEFAULT_CAST_CALLABLE_GETTER.copy() mapping.update({ SchemaType.STRING: self._unmarshal_string, + SchemaType.ANY: self._unmarshal_any, SchemaType.ARRAY: self._unmarshal_collection, SchemaType.OBJECT: self._unmarshal_object, }) @@ -137,9 +139,6 @@ class Schema(object): raise InvalidSchemaValue("Null value for non-nullable schema") return self.default - if self.type is None: - return value - cast_mapping = self.get_cast_mapping() if self.type is not SchemaType.STRING and value == '': @@ -156,8 +155,8 @@ class Schema(object): def unmarshal(self, value): """Unmarshal parameter from the value.""" if self.deprecated: - warnings.warn( - "The schema is deprecated", DeprecationWarning) + warnings.warn("The schema is deprecated", DeprecationWarning) + casted = self.cast(value) if casted is None and not self.required: @@ -190,7 +189,27 @@ class Schema(object): value, self.format) ) + def _unmarshal_any(self, value): + types_resolve_order = [ + SchemaType.OBJECT, SchemaType.ARRAY, SchemaType.BOOLEAN, + SchemaType.INTEGER, SchemaType.NUMBER, SchemaType.STRING, + ] + cast_mapping = self.get_cast_mapping() + for schema_type in types_resolve_order: + cast_callable = cast_mapping[schema_type] + try: + return cast_callable(value) + # @todo: remove ValueError when validation separated + except (OpenAPISchemaError, TypeError, ValueError): + continue + + raise NoValidSchema( + "No valid schema found for value {0}".format(value)) + def _unmarshal_collection(self, value): + if self.items is None: + raise UndefinedItemsSchema("Undefined items' schema") + return list(map(self.items.unmarshal, value)) def _unmarshal_object(self, value, model_factory=None): diff --git a/tests/integration/data/v3.0/petstore.yaml b/tests/integration/data/v3.0/petstore.yaml index c01ff6b..2dc3083 100644 --- a/tests/integration/data/v3.0/petstore.yaml +++ b/tests/integration/data/v3.0/petstore.yaml @@ -168,6 +168,12 @@ paths: $ref: "#/components/responses/ErrorResponse" components: schemas: + Utctime: + oneOf: + - type: string + enum: [always, now] + - type: string + format: date-time Address: type: object x-model: Address @@ -202,6 +208,7 @@ components: type: integer format: int64 PetCreate: + type: object x-model: PetCreate allOf: - $ref: "#/components/schemas/PetCreatePartOne" @@ -284,6 +291,8 @@ components: required: - name properties: + created: + $ref: "#/components/schemas/Utctime" name: type: string TagList: diff --git a/tests/integration/test_petstore.py b/tests/integration/test_petstore.py index e3b4fb9..3e3ed2a 100644 --- a/tests/integration/test_petstore.py +++ b/tests/integration/test_petstore.py @@ -17,6 +17,7 @@ from openapi_core.schema.request_bodies.models import RequestBody from openapi_core.schema.responses.models import Response from openapi_core.schema.schemas.exceptions import ( UndefinedSchemaProperty, MissingSchemaProperty, NoOneOfSchema, + NoValidSchema, ) from openapi_core.schema.schemas.models import Schema from openapi_core.schema.servers.exceptions import InvalidServer @@ -1032,3 +1033,143 @@ class TestPetstore(object): assert response_result.data.message == message assert response_result.data.rootCause == rootCause assert response_result.data.additionalinfo == additionalinfo + + def test_post_tags_created_now( + self, spec, response_validator): + host_url = 'http://petstore.swagger.io/v1' + path_pattern = '/v1/tags' + created = 'now' + pet_name = 'Dog' + data_json = { + 'created': created, + 'name': pet_name, + } + data = json.dumps(data_json) + + request = MockRequest( + host_url, 'POST', '/tags', + path_pattern=path_pattern, data=data, + ) + + parameters = request.get_parameters(spec) + body = request.get_body(spec) + + assert parameters == {} + assert isinstance(body, BaseModel) + assert body.created == created + assert body.name == pet_name + + code = 400 + message = 'Bad request' + rootCause = 'Tag already exist' + additionalinfo = 'Tag Dog already exist' + data_json = { + 'code': 400, + 'message': 'Bad request', + 'rootCause': 'Tag already exist', + 'additionalinfo': 'Tag Dog already exist', + } + data = json.dumps(data_json) + response = MockResponse(data, status_code=404) + + response_result = response_validator.validate(request, response) + + assert response_result.errors == [] + assert isinstance(response_result.data, BaseModel) + assert response_result.data.code == code + assert response_result.data.message == message + assert response_result.data.rootCause == rootCause + assert response_result.data.additionalinfo == additionalinfo + + def test_post_tags_created_datetime( + self, spec, response_validator): + host_url = 'http://petstore.swagger.io/v1' + path_pattern = '/v1/tags' + created = '2016-04-16T16:06:05Z' + pet_name = 'Dog' + data_json = { + 'created': created, + 'name': pet_name, + } + data = json.dumps(data_json) + + request = MockRequest( + host_url, 'POST', '/tags', + path_pattern=path_pattern, data=data, + ) + + parameters = request.get_parameters(spec) + body = request.get_body(spec) + + assert parameters == {} + assert isinstance(body, BaseModel) + assert body.created == created + assert body.name == pet_name + + code = 400 + message = 'Bad request' + rootCause = 'Tag already exist' + additionalinfo = 'Tag Dog already exist' + data_json = { + 'code': code, + 'message': message, + 'rootCause': rootCause, + 'additionalinfo': additionalinfo, + } + data = json.dumps(data_json) + response = MockResponse(data, status_code=404) + + response_result = response_validator.validate(request, response) + + assert response_result.errors == [] + assert isinstance(response_result.data, BaseModel) + assert response_result.data.code == code + assert response_result.data.message == message + assert response_result.data.rootCause == rootCause + assert response_result.data.additionalinfo == additionalinfo + + @pytest.mark.xfail(reason='OneOf for string not supported atm') + def test_post_tags_created_invalid_type( + self, spec, response_validator): + host_url = 'http://petstore.swagger.io/v1' + path_pattern = '/v1/tags' + created = 'long time ago' + pet_name = 'Dog' + data_json = { + 'created': created, + 'name': pet_name, + } + data = json.dumps(data_json) + + request = MockRequest( + host_url, 'POST', '/tags', + path_pattern=path_pattern, data=data, + ) + + parameters = request.get_parameters(spec) + with pytest.raises(NoValidSchema): + request.get_body(spec) + + assert parameters == {} + + code = 400 + message = 'Bad request' + rootCause = 'Tag already exist' + additionalinfo = 'Tag Dog already exist' + data_json = { + 'code': code, + 'message': message, + 'rootCause': rootCause, + 'additionalinfo': additionalinfo, + } + data = json.dumps(data_json) + response = MockResponse(data, status_code=404) + + response_result = response_validator.validate(request, response) + + assert response_result.errors == [] + assert isinstance(response_result.data, BaseModel) + assert response_result.data.code == code + assert response_result.data.message == message + assert response_result.data.rootCause == rootCause + assert response_result.data.additionalinfo == additionalinfo