Merge pull request #73 from p1c2u/feature/any-schema-type

Any schema type
This commit is contained in:
A 2018-08-23 09:40:42 +01:00 committed by GitHub
commit 632eb3e74a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 188 additions and 10 deletions

View file

@ -4,6 +4,7 @@ from enum import Enum
class SchemaType(Enum): class SchemaType(Enum):
ANY = None
INTEGER = 'integer' INTEGER = 'integer'
NUMBER = 'number' NUMBER = 'number'
STRING = 'string' STRING = 'string'

View file

@ -5,6 +5,14 @@ class OpenAPISchemaError(OpenAPIMappingError):
pass pass
class NoValidSchema(OpenAPISchemaError):
pass
class UndefinedItemsSchema(OpenAPISchemaError):
pass
class InvalidSchemaValue(OpenAPISchemaError): class InvalidSchemaValue(OpenAPISchemaError):
pass pass

View file

@ -16,7 +16,7 @@ class SchemaFactory(object):
def create(self, schema_spec): def create(self, schema_spec):
schema_deref = self.dereferencer.dereference(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') schema_format = schema_deref.get('format')
model = schema_deref.get('x-model', None) model = schema_deref.get('x-model', None)
required = schema_deref.get('required', False) required = schema_deref.get('required', False)

View file

@ -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.enums import SchemaFormat, SchemaType
from openapi_core.schema.schemas.exceptions import ( from openapi_core.schema.schemas.exceptions import (
InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty, InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty,
OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema, OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema, NoValidSchema,
UndefinedItemsSchema,
) )
from openapi_core.schema.schemas.util import ( from openapi_core.schema.schemas.util import (
forcebool, format_date, format_datetime, forcebool, format_date, format_datetime,
@ -46,7 +47,7 @@ class Schema(object):
} }
TYPE_VALIDATOR_CALLABLE_GETTER = { TYPE_VALIDATOR_CALLABLE_GETTER = {
None: lambda x: True, SchemaType.ANY: lambda x: x,
SchemaType.BOOLEAN: TypeValidator(bool), SchemaType.BOOLEAN: TypeValidator(bool),
SchemaType.INTEGER: TypeValidator(integer_types, exclude=bool), SchemaType.INTEGER: TypeValidator(integer_types, exclude=bool),
SchemaType.NUMBER: TypeValidator(integer_types, float, 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, schema_format=None, required=None, default=None, nullable=False,
enum=None, deprecated=False, all_of=None, one_of=None, enum=None, deprecated=False, all_of=None, one_of=None,
additional_properties=None): additional_properties=None):
self.type = schema_type and SchemaType(schema_type) self.type = SchemaType(schema_type)
self.model = model self.model = model
self.properties = properties and dict(properties) or {} self.properties = properties and dict(properties) or {}
self.items = items self.items = items
@ -107,7 +108,7 @@ class Schema(object):
return dict( return dict(
(prop_name, val) (prop_name, val)
for prop_name, val in all_properties.items() for prop_name, val in iteritems(all_properties)
if prop_name in required if prop_name in required
) )
@ -124,6 +125,7 @@ class Schema(object):
mapping = self.DEFAULT_CAST_CALLABLE_GETTER.copy() mapping = self.DEFAULT_CAST_CALLABLE_GETTER.copy()
mapping.update({ mapping.update({
SchemaType.STRING: self._unmarshal_string, SchemaType.STRING: self._unmarshal_string,
SchemaType.ANY: self._unmarshal_any,
SchemaType.ARRAY: self._unmarshal_collection, SchemaType.ARRAY: self._unmarshal_collection,
SchemaType.OBJECT: self._unmarshal_object, SchemaType.OBJECT: self._unmarshal_object,
}) })
@ -137,9 +139,6 @@ class Schema(object):
raise InvalidSchemaValue("Null value for non-nullable schema") raise InvalidSchemaValue("Null value for non-nullable schema")
return self.default return self.default
if self.type is None:
return value
cast_mapping = self.get_cast_mapping() cast_mapping = self.get_cast_mapping()
if self.type is not SchemaType.STRING and value == '': if self.type is not SchemaType.STRING and value == '':
@ -156,8 +155,8 @@ class Schema(object):
def unmarshal(self, value): def unmarshal(self, value):
"""Unmarshal parameter from the value.""" """Unmarshal parameter from the value."""
if self.deprecated: if self.deprecated:
warnings.warn( warnings.warn("The schema is deprecated", DeprecationWarning)
"The schema is deprecated", DeprecationWarning)
casted = self.cast(value) casted = self.cast(value)
if casted is None and not self.required: if casted is None and not self.required:
@ -190,7 +189,27 @@ class Schema(object):
value, self.format) 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): def _unmarshal_collection(self, value):
if self.items is None:
raise UndefinedItemsSchema("Undefined items' schema")
return list(map(self.items.unmarshal, value)) return list(map(self.items.unmarshal, value))
def _unmarshal_object(self, value, model_factory=None): def _unmarshal_object(self, value, model_factory=None):

View file

@ -168,6 +168,12 @@ paths:
$ref: "#/components/responses/ErrorResponse" $ref: "#/components/responses/ErrorResponse"
components: components:
schemas: schemas:
Utctime:
oneOf:
- type: string
enum: [always, now]
- type: string
format: date-time
Address: Address:
type: object type: object
x-model: Address x-model: Address
@ -202,6 +208,7 @@ components:
type: integer type: integer
format: int64 format: int64
PetCreate: PetCreate:
type: object
x-model: PetCreate x-model: PetCreate
allOf: allOf:
- $ref: "#/components/schemas/PetCreatePartOne" - $ref: "#/components/schemas/PetCreatePartOne"
@ -284,6 +291,8 @@ components:
required: required:
- name - name
properties: properties:
created:
$ref: "#/components/schemas/Utctime"
name: name:
type: string type: string
TagList: TagList:

View file

@ -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.responses.models import Response
from openapi_core.schema.schemas.exceptions import ( from openapi_core.schema.schemas.exceptions import (
UndefinedSchemaProperty, MissingSchemaProperty, NoOneOfSchema, UndefinedSchemaProperty, MissingSchemaProperty, NoOneOfSchema,
NoValidSchema,
) )
from openapi_core.schema.schemas.models import Schema from openapi_core.schema.schemas.models import Schema
from openapi_core.schema.servers.exceptions import InvalidServer 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.message == message
assert response_result.data.rootCause == rootCause assert response_result.data.rootCause == rootCause
assert response_result.data.additionalinfo == additionalinfo 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