diff --git a/cornice_swagger/converters/parameters.py b/cornice_swagger/converters/parameters.py index cd2cff7..a788e93 100644 --- a/cornice_swagger/converters/parameters.py +++ b/cornice_swagger/converters/parameters.py @@ -24,7 +24,7 @@ def convert(self, schema_node, definition_handler): schema = definition_handler(schema_node) # Parameters shouldn't have a title - schema.pop('title') + schema.pop('title', None) converted.update(schema) if schema.get('type') == 'array': diff --git a/cornice_swagger/converters/schema.py b/cornice_swagger/converters/schema.py index 45686cb..1d36fda 100644 --- a/cornice_swagger/converters/schema.py +++ b/cornice_swagger/converters/schema.py @@ -237,26 +237,33 @@ def convert_type(self, schema_node): class TypeConversionDispatcher(object): - converters = { - colander.Boolean: BooleanTypeConverter, - colander.Date: DateTypeConverter, - colander.DateTime: DateTimeTypeConverter, - colander.Float: NumberTypeConverter, - colander.Integer: IntegerTypeConverter, - colander.Mapping: ObjectTypeConverter, - colander.Sequence: ArrayTypeConverter, - colander.String: StringTypeConverter, - colander.Time: TimeTypeConverter, - } + def __init__(self, custom_converters={}, default_converter=None): + + self.converters = { + colander.Boolean: BooleanTypeConverter, + colander.Date: DateTypeConverter, + colander.DateTime: DateTimeTypeConverter, + colander.Float: NumberTypeConverter, + colander.Integer: IntegerTypeConverter, + colander.Mapping: ObjectTypeConverter, + colander.Sequence: ArrayTypeConverter, + colander.String: StringTypeConverter, + colander.Time: TimeTypeConverter, + } - def __call__(self, schema_node): + self.converters.update(custom_converters) + self.default_converter = default_converter + def __call__(self, schema_node): schema_type = schema_node.typ schema_type = type(schema_type) converter_class = self.converters.get(schema_type) if converter_class is None: - raise NoSuchConverter + if self.default_converter: + converter_class = self.default_converter + else: + raise NoSuchConverter converter = converter_class(self) converted = converter(schema_node) diff --git a/cornice_swagger/swagger.py b/cornice_swagger/swagger.py index f014267..f665cbb 100644 --- a/cornice_swagger/swagger.py +++ b/cornice_swagger/swagger.py @@ -1,13 +1,13 @@ """Cornice Swagger 2.0 documentor""" import re -from functools import partial import warnings import colander import six from cornice_swagger.util import body_schema_transformer, merge_dicts, trim -from cornice_swagger.converters import convert_schema, convert_parameter +from cornice_swagger.converters import (TypeConversionDispatcher as TypeConverter, + ParameterConversionDispatcher as ParameterConverter) class CorniceSwaggerException(Exception): @@ -19,7 +19,7 @@ class DefinitionHandler(object): json_pointer = '#/definitions/' - def __init__(self, ref=0): + def __init__(self, ref=0, type_converter=TypeConverter()): """ :param ref: The depth that should be used by self.ref when calling self.from_schema. @@ -27,6 +27,7 @@ def __init__(self, ref=0): self.definition_registry = {} self.ref = ref + self.type_converter = type_converter def from_schema(self, schema_node, base_name=None): """ @@ -40,7 +41,7 @@ def from_schema(self, schema_node, base_name=None): :rtype: dict Swagger schema. """ - return self._ref_recursive(convert_schema(schema_node), self.ref, base_name) + return self._ref_recursive(self.type_converter(schema_node), self.ref, base_name) def _ref_recursive(self, schema, depth, base_name=None): """ @@ -84,7 +85,9 @@ class ParameterHandler(object): json_pointer = '#/parameters/' - def __init__(self, definition_handler=DefinitionHandler(), ref=False): + def __init__(self, definition_handler=DefinitionHandler(), ref=False, + type_converter=TypeConverter(), + parameter_converter=ParameterConverter(TypeConverter())): """ :param definition_handler: Callable that handles swagger definition schemas. @@ -94,6 +97,8 @@ def __init__(self, definition_handler=DefinitionHandler(), ref=False): self.parameter_registry = {} + self.type_converter = type_converter + self.parameter_converter = parameter_converter self.definitions = definition_handler self.ref = ref @@ -117,9 +122,8 @@ def from_schema(self, schema_node): name = param_schema.__class__.__name__ if name == 'body': name = schema_node.__class__.__name__ + 'Body' - param = convert_parameter(location, - param_schema, - partial(self.definitions.from_schema, base_name=name)) + param = self.parameter_converter(location, + param_schema) param['name'] = name if self.ref: param = self._ref(param) @@ -127,9 +131,7 @@ def from_schema(self, schema_node): elif location in (('path', 'header', 'headers', 'querystring', 'GET')): for node_schema in param_schema.children: - param = convert_parameter(location, - node_schema, - self.definitions.from_schema) + param = self.parameter_converter(location, node_schema) if self.ref: param = self._ref(param) params.append(param) @@ -148,7 +150,7 @@ def from_path(self, path): params = [] for name in param_names: param_schema = colander.SchemaNode(colander.String(), name=name) - param = convert_parameter('path', param_schema) + param = self.parameter_converter('path', param_schema) if self.ref: param = self._ref(param) params.append(param) @@ -181,7 +183,8 @@ class ResponseHandler(object): json_pointer = '#/responses/' - def __init__(self, definition_handler=DefinitionHandler(), ref=False): + def __init__(self, definition_handler=DefinitionHandler(), + type_converter=TypeConverter(), ref=False): """ :param definition_handler: Callable that handles swagger definition schemas. @@ -191,6 +194,7 @@ def __init__(self, definition_handler=DefinitionHandler(), ref=False): self.response_registry = {} + self.type_converter = type_converter self.definitions = definition_handler self.ref = ref @@ -225,7 +229,7 @@ def from_schema_mapping(self, schema_mapping): response['schema'] = self.definitions.from_schema(field_schema) elif location in ('header', 'headers'): - header_schema = convert_schema(field_schema) + header_schema = self.type_converter(field_schema) headers = header_schema.get('properties') if headers: # Response headers doesn't accept titles @@ -285,6 +289,23 @@ class CorniceSwagger(object): """List of request schema transformers that should be applied to a request schema to make it comply with a cornice default request schema.""" + type_converter = TypeConverter + """Default :class:`cornice_swagger.converters.schema.TypeConversionDispatcher` + class used for converting colander schema Types to Swagger Types.""" + + parameter_converter = ParameterConverter + """Default :class:`cornice_swagger.converters.parameters.ParameterConversionDispatcher` + class used for converting colander/cornice request schemas to Swagger Parameters.""" + + custom_type_converters = {} + """Mapping for supporting custom types conversion on the default TypeConverter. + Should map `colander.TypeSchema` to `cornice_swagger.converters.schema.TypeConverter` + callables.""" + + default_type_converter = None + """Supplies a default type converter matching the interface of + `cornice_swagger.converters.schema.TypeConverter` to be used with unknown types.""" + default_tags = None """Provide a default list of tags or a callable that takes a cornice service and the method name (e.g GET) and returns a list of Swagger @@ -292,12 +313,12 @@ class CorniceSwagger(object): default_op_ids = None """Provide a callable that takes a cornice service and the method name - (e.g GET) and returns an operation Id that is used if an operation Id is + (e.g. GET) and returns an operation Id that is used if an operation Id is not provided. Each operation Id should be unique.""" default_security = None """Provide a default list or a callable that takes a cornice service and - the method name (e.g GET) and returns a list of OpenAPI security policies.""" + the method name (e.g. GET) and returns a list of OpenAPI security policies.""" summary_docstrings = False """Enable extracting operation summaries from view docstrings.""" @@ -346,13 +367,21 @@ def __init__(self, services=None, def_ref_depth=0, param_ref=False, resp_ref=Fal """ super(CorniceSwagger, self).__init__() + type_converter = self.type_converter(self.custom_type_converters, + self.default_type_converter) + parameter_converter = self.parameter_converter(type_converter) + if services is not None: self.services = services # Instantiate handlers - self.definitions = self.definitions(ref=def_ref_depth) - self.parameters = self.parameters(self.definitions, ref=param_ref) - self.responses = self.responses(self.definitions, ref=resp_ref) + self.definitions = self.definitions(ref=def_ref_depth, + type_converter=type_converter) + self.parameters = self.parameters(self.definitions, ref=param_ref, + type_converter=type_converter, + parameter_converter=parameter_converter) + self.responses = self.responses(self.definitions, ref=resp_ref, + type_converter=type_converter) def generate(self, title=None, version=None, base_path=None, info=None, swagger=None, **kwargs): diff --git a/tests/converters/test_schema.py b/tests/converters/test_schema.py index 6fd9504..bfae983 100644 --- a/tests/converters/test_schema.py +++ b/tests/converters/test_schema.py @@ -1,9 +1,13 @@ import unittest import colander + from cornice_swagger.converters import convert_schema as convert +from cornice_swagger.converters import TypeConversionDispatcher from cornice_swagger.converters.exceptions import NoSuchConverter +from ..support import AnyType, AnyTypeConverter + class ConversionTest(unittest.TestCase): @@ -21,6 +25,19 @@ def test_validate_all(self): 'minLength': 12, }) + def test_support_custom_converters(self): + node = colander.SchemaNode(AnyType()) + custom_converters = {AnyType: AnyTypeConverter} + converter = TypeConversionDispatcher(custom_converters) + ret = converter(node) + self.assertEquals(ret, {}) + + def test_support_default_converter(self): + node = colander.SchemaNode(AnyType()) + converter = TypeConversionDispatcher(default_converter=AnyTypeConverter) + ret = converter(node) + self.assertEquals(ret, {}) + def test_raise_no_such_converter_on_invalid_type(self): node = colander.SchemaNode(dict) self.assertRaises(NoSuchConverter, convert, node) diff --git a/tests/support.py b/tests/support.py index 2e7079f..08145ed 100644 --- a/tests/support.py +++ b/tests/support.py @@ -1,5 +1,7 @@ import colander +from cornice_swagger.converters.schema import TypeConverter + class MyNestedSchema(colander.MappingSchema): my_precious = colander.SchemaNode(colander.Boolean()) @@ -55,3 +57,14 @@ class AnotherDeclarativeSchema(colander.MappingSchema): @colander.instantiate(description='my another body') class body(colander.MappingSchema): timestamp = colander.SchemaNode(colander.Int()) + + +class AnyType(colander.SchemaType): + """A simple custom colander type.""" + def deserialize(self, cstruct=colander.null): + return cstruct + + +class AnyTypeConverter(TypeConverter): + def __call__(self, schema_node): + return {} diff --git a/tests/test_swagger.py b/tests/test_swagger.py index 8b32bb4..3653d57 100644 --- a/tests/test_swagger.py +++ b/tests/test_swagger.py @@ -6,6 +6,7 @@ from flex.core import validate from cornice_swagger.swagger import CorniceSwagger, CorniceSwaggerException + from .support import (GetRequestSchema, PutRequestSchema, response_schemas, BodySchema, HeaderSchema)