diff --git a/.gitignore b/.gitignore index 2350847..2ef7406 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ site/ __pycache__ .cache .coverage +.idea/ diff --git a/openapi_codec/encode.py b/openapi_codec/encode.py index 13b3ef4..e752f40 100644 --- a/openapi_codec/encode.py +++ b/openapi_codec/encode.py @@ -110,6 +110,17 @@ def _get_field_description(field): return field.schema.description +def _get_schema_type(schema): + return { + coreschema.String: 'string', + coreschema.Integer: 'integer', + coreschema.Number: 'number', + coreschema.Boolean: 'boolean', + coreschema.Array: 'array', + coreschema.Object: 'object', + }.get(schema.__class__, 'string') + + def _get_field_type(field): if getattr(field, 'type', None) is not None: # Deprecated @@ -118,14 +129,30 @@ def _get_field_type(field): if field.schema is None: return 'string' - return { - coreschema.String: 'string', - coreschema.Integer: 'integer', - coreschema.Number: 'number', - coreschema.Boolean: 'boolean', - coreschema.Array: 'array', - coreschema.Object: 'object', - }.get(field.schema.__class__, 'string') + return _get_schema_type(field.schema) + + +def _get_array_schema_items(field): + array_items = {} + + if isinstance(field.schema.items, list): + raise TypeError('Swagger 2.0 spec does not allow the items property to be a list of item types.') + + item_type = field.schema.items + if not isinstance(item_type, coreschema.Anything): + array_items['type'] = _get_schema_type(item_type) or 'string' + if isinstance(item_type, coreschema.Object) and field.schema.items.properties is not None: + # This is an Array of Objects + array_props = {} + for prop_key, prop_value in field.schema.items.properties.items(): + array_props[prop_key] = { + 'description': getattr(prop_value, 'description', ''), + 'type': _get_schema_type(prop_value) or 'string', + } + + array_items['properties'] = array_props + + return array_items def _get_parameters(link, encoding): @@ -151,7 +178,7 @@ def _get_parameters(link, encoding): 'type': field_type, } if field_type == 'array': - parameter['items'] = {'type': 'string'} + parameter['items'] = _get_array_schema_items(field) parameters.append(parameter) else: # Expand coreapi fields with location='form' into a single swagger @@ -162,22 +189,27 @@ def _get_parameters(link, encoding): 'type': field_type, } if field_type == 'array': - schema_property['items'] = {'type': 'string'} + schema_property['items'] = _get_array_schema_items(field) properties[field.name] = schema_property if field.required: required.append(field.name) elif location == 'body': + schema = { + 'type': field_type or 'string', + } + if encoding == 'application/octet-stream': # https://github.com/OAI/OpenAPI-Specification/issues/50#issuecomment-112063782 schema = {'type': 'string', 'format': 'binary'} - else: - schema = {} + elif field_type == 'array': + schema['items'] = _get_array_schema_items(field) + parameter = { 'name': field.name, 'required': field.required, 'in': location, 'description': field_description, - 'schema': schema + 'schema': schema, } parameters.append(parameter) else: @@ -189,7 +221,7 @@ def _get_parameters(link, encoding): 'type': field_type or 'string', } if field_type == 'array': - parameter['items'] = {'type': 'string'} + parameter['items'] = _get_array_schema_items(field) parameters.append(parameter) if properties: diff --git a/tests/test_encode.py b/tests/test_encode.py index 429a9f0..9fe221c 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -106,3 +106,26 @@ def test_expected_fields(self): 'type': 'string' # Everything is a string for now. } self.assertEquals(self.swagger[0], expected) + + +class TestDeprecatedFieldTypeParameters(TestCase): + def setUp(self): + self.field = coreapi.Field( + name='email', + required=True, + location='query', + description='A valid email address.', + type='string', + ) + self.swagger = _get_parameters(coreapi.Link(fields=[self.field]), encoding='') + + def test_expected_fields(self): + self.assertEquals(len(self.swagger), 1) + expected = { + 'name': self.field.name, + 'required': self.field.required, + 'in': 'query', + 'description': self.field.description, + 'type': 'string' # Everything is a string for now. + } + assert self.swagger[0] == expected diff --git a/tests/test_encode_arrays.py b/tests/test_encode_arrays.py new file mode 100644 index 0000000..ea0af4d --- /dev/null +++ b/tests/test_encode_arrays.py @@ -0,0 +1,201 @@ +import coreapi +import coreschema +from openapi_codec.encode import _get_parameters, _get_schema_type +from unittest import TestCase + + +def make_array_json(json_template, array_schema, location, field=None): + if location == 'body': + # In Swagger 2.0, arrays in the body are defined in the schema attribute + schema = { + 'schema': array_schema + } + for key in schema: + json_template[key] = schema[key] + elif location == 'form': + json_template['schema']['properties'][field.name] = array_schema + else: + # In Swagger 2.0, arrays not in the body are defined right in the field properties + schema = array_schema + for key in schema: + json_template[key] = schema[key] + + return json_template + + +class TestArrayParameters(TestCase): + def setUp(self): + self.maxDiff = None + self.encoding = '' + + self.definitions = [] + for location in ['body', 'query']: + field = coreapi.Field( + name='data', + required=True, + location=location, + description='Array of Anything', + schema=coreschema.Array() + ) + self.definitions.append(dict( + python=field, + json=make_array_json({ + 'name': field.name, + 'required': field.required, + 'in': location, + 'description': field.description, + }, { + 'type': 'array', + 'items': {}, + }, location) + )) + + for schema_type in coreschema.__all__: + schema = None + native_type = None + + try: + schema = schema_type() + native_type = _get_schema_type(schema) + except Exception: + pass + + if native_type is not None and (isinstance(schema_type, coreschema.String) or native_type != 'string'): + field = coreapi.Field( + name='data', + required=True, + location=location, + description='Array of %s' % native_type.capitalize() + 's', + schema=coreschema.Array(items=schema) + ) + self.definitions.append(dict( + python=field, + json=make_array_json({ + 'name': field.name, + 'required': field.required, + 'in': location, + 'description': field.description, + }, { + 'type': 'array', + 'items': { + 'type': native_type, + } + }, location) + )) + + field = coreapi.Field( + name='data', + required=True, + location=location, + description='Array of Objects with Properties', + schema=coreschema.Array( + items=coreschema.Object( + properties={ + 'id': coreschema.Integer(), + } + ) + ) + ) + + self.definitions.append(dict( + python=field, + json=make_array_json({ + 'name': field.name, + 'required': field.required, + 'in': location, + 'description': field.description, + }, { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'id': { + 'description': '', + 'type': 'integer' + }, + }, + } + }, location) + )) + + def test_expected_path_fields(self): + for d in self.definitions: + swagger = _get_parameters(coreapi.Link(fields=[d['python']]), encoding=self.encoding) + self.assertEquals(swagger[0], d['json'], msg='Encoded JSON value didn\'t match for %s' % d['python'].description) + + def test_unsupported_item_type(self): + field = coreapi.Field( + name='data', + required=True, + location='body', + description='Array of Strings and Integers', + schema=coreschema.Array( + items=[coreschema.String(), coreschema.Integer()], + ) + ) + + with self.assertRaises(TypeError): + _get_parameters(coreapi.Link(fields=[field]), encoding=self.encoding) + + +class TestArrayFormParameters(TestArrayParameters): + def setUp(self): + self.maxDiff = None + self.encoding = '' + self.definitions = [] + + location = 'form' + + field = coreapi.Field( + name='data', + required=True, + location=location, + description='Array of Anything', + schema=coreschema.Array() + ) + self.definitions.append(dict( + python=field, + json=make_array_json({ + 'name': field.name, + 'in': 'body', + 'schema': { + 'required': [field.name], + 'type': 'object', + 'properties': {}, + }, + }, { + 'type': 'array', + 'description': field.description, + 'items': {}, + }, location, field) + )) + + +class TestArrayEncodedFormParameters(TestArrayParameters): + def setUp(self): + self.maxDiff = None + self.encoding = 'multipart/form-data' + self.definitions = [] + + location = 'form' + swagger_location = 'formData' + + field = coreapi.Field( + name='data', + required=True, + location=location, + description='Array of Anything', + schema=coreschema.Array() + ) + self.definitions.append(dict( + python=field, + json=make_array_json({ + 'name': field.name, + 'required': field.required, + 'in': swagger_location, + 'description': field.description, + }, { + 'type': 'array', + 'items': {}, + }, swagger_location) + )) diff --git a/tox.ini b/tox.ini index 3e9707d..e1ba79f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py35,py34,py27 +envlist = py37,py36,py35,py34,py27 [testenv] deps = -rrequirements.txt commands = ./runtests