Skip to content

Commit

Permalink
Allow custom converters from CorniceSwagger class
Browse files Browse the repository at this point in the history
This introduces a way to set custom type converters straight from
the CorniceSwagger class. It supports both a default type or a
mapping of custom types. This allows setting additional type
converters without having to patch the TypeConversionDispatcher
class.

Also depracates the `converters.convert_xxx` functions.

Replaces: #48
Related to: #46
  • Loading branch information
gabisurita committed Feb 13, 2017
1 parent ae929ad commit b7d08be
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 33 deletions.
2 changes: 1 addition & 1 deletion cornice_swagger/converters/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
33 changes: 20 additions & 13 deletions cornice_swagger/converters/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
67 changes: 48 additions & 19 deletions cornice_swagger/swagger.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -19,14 +19,15 @@ 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.
"""

self.definition_registry = {}
self.ref = ref
self.type_converter = type_converter

def from_schema(self, schema_node, base_name=None):
"""
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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.
Expand All @@ -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

Expand All @@ -117,19 +122,16 @@ 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)
params.append(param)

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)
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -285,19 +289,36 @@ 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
tags to be used if not provided by the view."""

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."""
Expand Down Expand Up @@ -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):
Expand Down
17 changes: 17 additions & 0 deletions tests/converters/test_schema.py
Original file line number Diff line number Diff line change
@@ -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):

Expand All @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions tests/support.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import colander

from cornice_swagger.converters.schema import TypeConverter


class MyNestedSchema(colander.MappingSchema):
my_precious = colander.SchemaNode(colander.Boolean())
Expand Down Expand Up @@ -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 {}
1 change: 1 addition & 0 deletions tests/test_swagger.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down

0 comments on commit b7d08be

Please sign in to comment.