Пример #1
0
    def __init__(self,
                 processor_class: typing.Optional[
                     typing.Type[Processor]] = None,
                 async_: bool = False) -> None:
        self._buffer = DecorationBuffer()
        self._controllers = []  # type: typing.List[DecoratedController]
        self._context = None  # type: ContextInterface
        self._error_builder = None  # type: ErrorBuilderInterface
        self._async = async_
        self.doc_generator = DocGenerator()

        self.logger = logging.getLogger(LOGGER_NAME)
        self.logger.debug("Create new Hapic instance")

        # This local function will be pass to different components
        # who will need context but declared (like with decorator)
        # before context declaration
        def context_getter():
            return self._context

        # This local function will be pass to different components
        # who will need error_builder but declared (like with decorator)
        # before error_builder declaration
        def error_builder_getter():
            return self._context.default_error_builder

        self._processor_class = processor_class
        self._context_getter = context_getter
        self._error_builder_getter = error_builder_getter
Пример #2
0
    def __init__(self):
        self._buffer = DecorationBuffer()
        self._controllers = []  # type: typing.List[DecoratedController]
        self._context = None  # type: ContextInterface

        # This local function will be pass to different components
        # who will need context but declared (like with decorator)
        # before context declaration
        def context_getter():
            return self._context

        self._context_getter = context_getter
Пример #3
0
    def test_unit__buffer_usage__error__cant_replace(self):
        buffer = DecorationBuffer()

        input_path_description = InputPathDescription(fake_controller_wrapper)
        input_query_description = InputQueryDescription(fake_controller_wrapper)
        input_body_description = InputBodyDescription(fake_controller_wrapper)
        input_headers_description = InputHeadersDescription(fake_controller_wrapper)
        input_forms_description = InputFormsDescription(fake_controller_wrapper)
        output_headers_description = OutputHeadersDescription(fake_controller_wrapper)
        output_body_description = OutputBodyDescription(fake_controller_wrapper)

        buffer.input_path = input_path_description
        buffer.input_query = input_query_description
        buffer.input_body = input_body_description
        buffer.input_headers = input_headers_description
        buffer.input_forms = input_forms_description
        buffer.output_headers = output_headers_description
        buffer.output_body = output_body_description

        with pytest.raises(AlreadyDecoratedException):
            buffer.input_path = input_path_description

        with pytest.raises(AlreadyDecoratedException):
            buffer.input_query = input_query_description

        with pytest.raises(AlreadyDecoratedException):
            buffer.input_body = input_body_description

        with pytest.raises(AlreadyDecoratedException):
            buffer.input_headers = input_headers_description

        with pytest.raises(AlreadyDecoratedException):
            buffer.input_forms = input_forms_description

        with pytest.raises(AlreadyDecoratedException):
            buffer.output_headers = output_headers_description

        with pytest.raises(AlreadyDecoratedException):
            buffer.output_body = output_body_description
Пример #4
0
    def test_unit__buffer_usage__ok__set_descriptions(self):
        buffer = DecorationBuffer()

        input_path_description = InputPathDescription(fake_controller_wrapper)
        input_query_description = InputQueryDescription(fake_controller_wrapper)
        input_body_description = InputBodyDescription(fake_controller_wrapper)
        input_headers_description = InputHeadersDescription(fake_controller_wrapper)
        input_forms_description = InputFormsDescription(fake_controller_wrapper)
        output_headers_description = OutputHeadersDescription(fake_controller_wrapper)
        output_body_description = OutputBodyDescription(fake_controller_wrapper)
        error_description = ErrorDescription(fake_controller_wrapper)

        buffer.input_path = input_path_description
        buffer.input_query = input_query_description
        buffer.input_body = input_body_description
        buffer.input_headers = input_headers_description
        buffer.input_forms = input_forms_description
        buffer.output_headers = output_headers_description
        buffer.output_body = output_body_description
        buffer.add_error(error_description)

        description = buffer.get_description()
        assert description.input_path == input_path_description
        assert description.input_query == input_query_description
        assert description.input_body == input_body_description
        assert description.input_headers == input_headers_description
        assert description.input_forms == input_forms_description
        assert description.output_headers == output_headers_description
        assert description.output_body == output_body_description
        assert description.errors == [error_description]

        buffer.clear()
        description = buffer.get_description()

        assert description.input_path is None
        assert description.input_query is None
        assert description.input_body is None
        assert description.input_headers is None
        assert description.input_forms is None
        assert description.output_headers is None
        assert description.output_body is None
        assert description.errors == []
Пример #5
0
class Hapic(object):
    def __init__(self,
                 processor_class: typing.Optional[
                     typing.Type[Processor]] = None,
                 async_: bool = False) -> None:
        self._buffer = DecorationBuffer()
        self._controllers = []  # type: typing.List[DecoratedController]
        self._context = None  # type: ContextInterface
        self._error_builder = None  # type: ErrorBuilderInterface
        self._async = async_
        self.doc_generator = DocGenerator()

        self.logger = logging.getLogger(LOGGER_NAME)
        self.logger.debug("Create new Hapic instance")

        # This local function will be pass to different components
        # who will need context but declared (like with decorator)
        # before context declaration
        def context_getter():
            return self._context

        # This local function will be pass to different components
        # who will need error_builder but declared (like with decorator)
        # before error_builder declaration
        def error_builder_getter():
            return self._context.default_error_builder

        self._processor_class = processor_class
        self._context_getter = context_getter
        self._error_builder_getter = error_builder_getter

    @property
    def ready(self) -> bool:
        """
        :return: True if hapic have a processor_class, else False
        """
        return self._processor_class is not None

    @property
    def processor_class(self) -> typing.Type[Processor]:
        if self._processor_class is None:
            raise ConfigurationException(
                "You must define Hapic processor_class before to use it.")

        return self._processor_class

    @property
    def controllers(self) -> typing.List[DecoratedController]:
        return self._controllers

    @property
    def context(self) -> ContextInterface:
        return self._context

    def set_context(self, context: ContextInterface) -> None:
        assert not self._context
        self._context = context
        self._context.set_processor_class(self.processor_class)

        try:
            self._context.default_error_builder
        except ConfigurationException:
            self._context.default_error_builder = self.processor_class.get_default_error_builder(
            )

    def reset_context(self) -> None:
        self._context = None

    def set_processor_class(self,
                            processor_class: typing.Type[Processor]) -> None:
        self._processor_class = processor_class

    def _get_processor_factory(
            self,
            schema: typing.Any,
            processor: Processor = None) -> typing.Callable[[], Processor]:
        """
        :param schema: Schema to be give to final processor instance
        :param processor: Optional Processor instance. If no given,
            use hapic default processor class instance
        :return: A callable able to return an Processor instance
        """
        if processor is not None:

            def get_processor():
                processor.set_schema(schema)
                return processor

            return get_processor

        def get_default_processor():
            processor_ = self._processor_class()
            processor_.set_schema(schema)
            return processor_

        return get_default_processor

    def with_api_doc(self,
                     tags: typing.List["str"] = None,
                     disable_doc: bool = False,
                     deprecated: bool = False):
        """
        Permit to generate doc about a controller. Use as a decorator:

        ```
        @hapic.with_api_doc()
        def my_controller(self):
            # ...
        ```

        What it do: Register this controller with all previous given
        information like `@hapic.input_path(...)` etc.

        :param tags: list of string tags (OpenApi)
        :return: The decorator
        """
        # FIXME BS 20171228: Documenter sur ce que ça fait vraiment (tester:
        # on peut l'enlever si on veut pas generer la doc ?)
        tags = tags or []  # FDV

        def decorator(func):
            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                return func(*args, **kwargs)

            token = uuid.uuid4().hex
            self.logger.debug('Collect description of "{}" and mark '
                              'it with token "{}"'.format(str(func), token))

            setattr(wrapper, DECORATION_ATTRIBUTE_NAME, token)
            setattr(func, DECORATION_ATTRIBUTE_NAME, token)

            description = self._buffer.get_description()
            description.tags = tags
            description.disable_doc = disable_doc
            description.deprecated = deprecated

            reference = ControllerReference(wrapper=wrapper,
                                            wrapped=func,
                                            token=token)
            decorated_controller = DecoratedController(reference=reference,
                                                       description=description,
                                                       name=func.__name__)
            self._buffer.clear()
            self._controllers.append(decorated_controller)
            return wrapper

        return decorator

    def output_body(
        self,
        schema: typing.Any,
        processor: Processor = None,
        context: ContextInterface = None,
        error_http_code: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
        default_http_code: HTTPStatus = HTTPStatus.OK,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        processor_factory = self._get_processor_factory(schema, processor)
        context = context or self._context_getter

        if self._async:
            decoration = AsyncOutputBodyControllerWrapper(
                context=context,
                processor_factory=processor_factory,
                error_http_code=error_http_code,
                default_http_code=default_http_code,
            )
        else:
            decoration = OutputBodyControllerWrapper(
                context=context,
                processor_factory=processor_factory,
                error_http_code=error_http_code,
                default_http_code=default_http_code,
            )

        def decorator(func):
            self._buffer.output_body = OutputBodyDescription(decoration)
            return decoration.get_wrapper(func)

        return decorator

    def output_stream(
        self,
        item_schema: typing.Any,
        processor: Processor = None,
        context: ContextInterface = None,
        error_http_code: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
        default_http_code: HTTPStatus = HTTPStatus.OK,
        ignore_on_error: bool = True,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        """
        Decorate with a wrapper who check and serialize each items in output
        stream.

        :param item_schema: Schema of output stream items
        :param processor: Processor object to process with given
        schema
        :param context: Context to use here
        :param error_http_code: http code in case of error
        :param default_http_code: http code in case of success
        :param ignore_on_error: if set, an error of serialization will be
        ignored: stream will not send this failed object
        :return: decorator
        """
        processor_factory = self._get_processor_factory(item_schema, processor)
        context = context or self._context_getter

        if self._async:
            decoration = AsyncOutputStreamControllerWrapper(
                context=context,
                processor_factory=processor_factory,
                error_http_code=error_http_code,
                default_http_code=default_http_code,
                ignore_on_error=ignore_on_error,
            )
        else:
            # TODO BS 2018-07-25: To do
            raise NotImplementedError("todo")

        def decorator(func):
            self._buffer.output_stream = OutputStreamDescription(decoration)
            return decoration.get_wrapper(func)

        return decorator

    def output_headers(
        self,
        schema: typing.Any,
        processor: Processor = None,
        context: ContextInterface = None,
        error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
        default_http_code: HTTPStatus = HTTPStatus.OK,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        processor_factory = self._get_processor_factory(schema, processor)
        context = context or self._context_getter

        decoration = OutputHeadersControllerWrapper(
            context=context,
            processor_factory=processor_factory,
            error_http_code=error_http_code,
            default_http_code=default_http_code,
        )

        def decorator(func):
            self._buffer.output_headers = OutputHeadersDescription(decoration)
            return decoration.get_wrapper(func)

        return decorator

    # TODO BS 20171102: Think about possibilities to validate output ?
    # (with mime type, or validator)
    def output_file(
        self,
        output_types: typing.List[str],
        processor: Processor = None,
        context: ContextInterface = None,
        default_http_code: HTTPStatus = HTTPStatus.OK,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        # TODO BS 2018-11-16: This is not smart to give None instead schema
        processor_factory = self._get_processor_factory(None, processor)
        context = context or self._context_getter
        if self._async:
            decoration = AsyncOutputFileControllerWrapper(
                context=context,
                processor_factory=processor_factory,
                output_types=output_types,
                default_http_code=default_http_code,
            )
        else:
            decoration = OutputFileControllerWrapper(
                context=context,
                processor_factory=processor_factory,
                output_types=output_types,
                default_http_code=default_http_code,
            )

        def decorator(func):
            self._buffer.output_file = OutputFileDescription(decoration)
            return decoration.get_wrapper(func)

        return decorator

    def input_headers(
        self,
        schema: typing.Any,
        processor: Processor = None,
        context: ContextInterface = None,
        error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
        default_http_code: HTTPStatus = HTTPStatus.OK,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        processor_factory = self._get_processor_factory(schema, processor)
        context = context or self._context_getter

        decoration = InputHeadersControllerWrapper(
            context=context,
            processor_factory=processor_factory,
            error_http_code=error_http_code,
            default_http_code=default_http_code,
        )

        def decorator(func):
            self._buffer.input_headers = InputHeadersDescription(decoration)
            return decoration.get_wrapper(func)

        return decorator

    def input_path(
        self,
        schema: typing.Any,
        processor: Processor = None,
        context: ContextInterface = None,
        error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
        default_http_code: HTTPStatus = HTTPStatus.OK,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        processor_factory = self._get_processor_factory(schema, processor)
        context = context or self._context_getter

        if self._async:
            decoration = AsyncInputPathControllerWrapper(
                context=context,
                processor_factory=processor_factory,
                error_http_code=error_http_code,
                default_http_code=default_http_code,
            )
        else:
            decoration = InputPathControllerWrapper(
                context=context,
                processor_factory=processor_factory,
                error_http_code=error_http_code,
                default_http_code=default_http_code,
            )

        def decorator(func):
            self._buffer.input_path = InputPathDescription(decoration)
            return decoration.get_wrapper(func)

        return decorator

    def input_query(
        self,
        schema: typing.Any,
        processor: Processor = None,
        context: ContextInterface = None,
        error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
        default_http_code: HTTPStatus = HTTPStatus.OK,
        as_list: typing.List[str] = None,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        processor_factory = self._get_processor_factory(schema, processor)
        context = context or self._context_getter

        if self._async:
            decoration = AsyncInputQueryControllerWrapper(
                context=context,
                processor_factory=processor_factory,
                error_http_code=error_http_code,
                default_http_code=default_http_code,
                as_list=as_list,
            )
        else:
            decoration = InputQueryControllerWrapper(
                context=context,
                processor_factory=processor_factory,
                error_http_code=error_http_code,
                default_http_code=default_http_code,
                as_list=as_list,
            )

        def decorator(func):
            self._buffer.input_query = InputQueryDescription(decoration)
            return decoration.get_wrapper(func)

        return decorator

    def input_body(
        self,
        schema: typing.Any,
        processor: Processor = None,
        context: ContextInterface = None,
        error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
        default_http_code: HTTPStatus = HTTPStatus.OK,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        processor_factory = self._get_processor_factory(schema, processor)
        context = context or self._context_getter

        if self._async:
            decoration = AsyncInputBodyControllerWrapper(
                context=context,
                processor_factory=processor_factory,
                error_http_code=error_http_code,
                default_http_code=default_http_code,
            )
        else:
            decoration = InputBodyControllerWrapper(
                context=context,
                processor_factory=processor_factory,
                error_http_code=error_http_code,
                default_http_code=default_http_code,
            )

        def decorator(func):
            self._buffer.input_body = InputBodyDescription(decoration)
            return decoration.get_wrapper(func)

        return decorator

    def input_forms(
        self,
        schema: typing.Any,
        processor: Processor = None,
        context: ContextInterface = None,
        error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
        default_http_code: HTTPStatus = HTTPStatus.OK,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        processor_factory = self._get_processor_factory(schema, processor)
        context = context or self._context_getter

        decoration = InputFormsControllerWrapper(
            context=context,
            processor_factory=processor_factory,
            error_http_code=error_http_code,
            default_http_code=default_http_code,
        )

        def decorator(func):
            self._buffer.input_forms = InputFormsDescription(decoration)
            return decoration.get_wrapper(func)

        return decorator

    def input_files(
        self,
        schema: typing.Any,
        processor: Processor = None,
        context: ContextInterface = None,
        error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
        default_http_code: HTTPStatus = HTTPStatus.OK,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        processor_factory = self._get_processor_factory(schema, processor)
        context = context or self._context_getter

        if self._async:
            decoration = AsyncInputFilesControllerWrapper(
                context=context,
                processor_factory=processor_factory,
                error_http_code=error_http_code,
                default_http_code=default_http_code,
            )
        else:
            decoration = InputFilesControllerWrapper(
                context=context,
                processor_factory=processor_factory,
                error_http_code=error_http_code,
                default_http_code=default_http_code,
            )

        def decorator(func):
            self._buffer.input_files = InputFilesDescription(decoration)
            return decoration.get_wrapper(func)

        return decorator

    def handle_exception(
        self,
        handled_exception_class: typing.Type[Exception] = Exception,
        http_code: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
        error_builder: ErrorBuilderInterface = None,
        context: ContextInterface = None,
        description: str = None,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        context = context or self._context_getter
        error_builder = error_builder or self._error_builder_getter

        if self._async:
            decoration = AsyncExceptionHandlerControllerWrapper(
                handled_exception_class,
                context,
                error_builder=error_builder,
                http_code=http_code,
                description=description,
                # We must give a processor factory because wrapper will check
                # it's own error format
                processor_factory=lambda schema_: self.processor_class(schema_
                                                                       ),
            )

        else:
            decoration = ExceptionHandlerControllerWrapper(
                handled_exception_class,
                context,
                error_builder=error_builder,
                http_code=http_code,
                description=description,
                # We must give a processor factory because wrapper will check
                # it's own error format
                processor_factory=lambda schema_: self.processor_class(schema_
                                                                       ),
            )

        def decorator(func):
            self._buffer.errors.append(ErrorDescription(decoration))
            return decoration.get_wrapper(func)

        return decorator

    def generate_doc(
        self,
        title: str = "",
        description: str = "",
        version: str = "1.0.0",
        wildcard_method_replacement: typing.Optional[
            typing.List["str"]] = None,
    ) -> dict:
        """
        See hapic.doc.DocGenerator#get_doc docstring
        :param title: Title of generated doc
        :param description: Description of generated doc
        :param wildcard_method_replacement: If wild card found as method in
        operations consider these given methods as replacement. If not provided
        all OpenAPI v2 valid methods will be used.
        :return: dict containing apispec doc
        """
        return self.doc_generator.get_doc(
            self,
            self._controllers,
            self.context,
            title=title,
            description=description,
            version=version,
            wildcard_method_replacement=wildcard_method_replacement,
        )

    def save_doc_in_file(self,
                         file_path: str,
                         title: str = "",
                         description: str = "") -> None:
        """
        See hapic.doc.DocGenerator#get_doc docstring
        :param file_path: The file path to write doc in YAML format
        :param title: Title of generated doc
        :param description: Description of generated doc
        """
        self.doc_generator.save_in_file(
            self,
            file_path,
            controllers=self._controllers,
            context=self.context,
            title=title,
            description=description,
        )

    def add_documentation_view(self,
                               route: str,
                               title: str = "",
                               description: str = "") -> None:
        # Ensure "/" at end of route, else web browser will not consider it as
        # a path
        if not route.endswith("/"):
            route = "{}/".format(route)

        swaggerui_path = os.path.join(
            os.path.dirname(os.path.abspath(__file__)), "static", "swaggerui")

        # Documentation file view
        doc_yaml = self.doc_generator.get_doc_yaml(
            hapic=self,
            controllers=self._controllers,
            context=self.context,
            title=title,
            description=description,
        )

        def spec_yaml_view(*args, **kwargs):
            """
            Method to return swagger generated yaml spec file.

            This method will be call as a framework view, like those,
            it need to handle the default arguments of a framework view.
            As frameworks have different arguments patterns, we should
            allow any arguments patterns (args, kwargs).
            """
            return self.context.get_response(doc_yaml,
                                             mimetype="text/x-yaml",
                                             http_code=HTTPStatus.OK)

        # Prepare views html content
        doc_index_path = os.path.join(swaggerui_path, "index.html")
        with open(doc_index_path, "r") as doc_page:
            doc_page_content = doc_page.read()
        doc_page_content = doc_page_content.replace("{{ spec_uri }}",
                                                    "spec.yml")

        # Declare the swaggerui view
        def api_doc_view(*args, **kwargs):
            """
            Method to return html index view of swagger ui.

            This method will be call as a framework view, like those,
            it need to handle the default arguments of a framework view.
            As frameworks have different arguments patterns, we should
            allow any arguments patterns (args, kwargs).
            """
            return self.context.get_response(doc_page_content,
                                             http_code=HTTPStatus.OK,
                                             mimetype="text/html")

        # Add a view to generate the html index page of swagger-ui
        self.context.add_view(route=route,
                              http_method="GET",
                              view_func=api_doc_view)

        # Add a doc yaml view
        self.context.add_view(route=os.path.join(route, "spec.yml"),
                              http_method="GET",
                              view_func=spec_yaml_view)

        # Add swagger directory as served static dir
        self.context.serve_directory(route, swaggerui_path)
Пример #6
0
class Hapic(object):
    def __init__(self):
        self._buffer = DecorationBuffer()
        self._controllers = []  # type: typing.List[DecoratedController]
        self._context = None  # type: ContextInterface

        # This local function will be pass to different components
        # who will need context but declared (like with decorator)
        # before context declaration
        def context_getter():
            return self._context

        self._context_getter = context_getter

        # TODO: Permettre la surcharge des classes utilisés ci-dessous, see #14

    @property
    def controllers(self) -> typing.List[DecoratedController]:
        return self._controllers

    @property
    def context(self) -> ContextInterface:
        return self._context

    def with_api_doc(self):
        def decorator(func):
            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                return func(*args, **kwargs)

            token = uuid.uuid4().hex
            setattr(wrapper, DECORATION_ATTRIBUTE_NAME, token)
            setattr(func, DECORATION_ATTRIBUTE_NAME, token)

            description = self._buffer.get_description()

            reference = ControllerReference(
                wrapper=wrapper,
                wrapped=func,
                token=token,
            )
            decorated_controller = DecoratedController(
                reference=reference,
                description=description,
                name=func.__name__,
            )
            self._buffer.clear()
            self._controllers.append(decorated_controller)
            return wrapper

        return decorator

    def set_context(self, context: ContextInterface) -> None:
        assert not self._context
        self._context = context

    def output_body(
        self,
        schema: typing.Any,
        processor: ProcessorInterface = None,
        context: ContextInterface = None,
        error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
        default_http_code: HTTPStatus = HTTPStatus.OK,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        processor = processor or MarshmallowOutputProcessor()
        processor.schema = schema
        context = context or self._context_getter
        decoration = OutputBodyControllerWrapper(
            context=context,
            processor=processor,
            error_http_code=error_http_code,
            default_http_code=default_http_code,
        )

        def decorator(func):
            self._buffer.output_body = OutputBodyDescription(decoration)
            return decoration.get_wrapper(func)
        return decorator

    def output_headers(
        self,
        schema: typing.Any,
        processor: ProcessorInterface = None,
        context: ContextInterface = None,
        error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
        default_http_code: HTTPStatus = HTTPStatus.OK,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        processor = processor or MarshmallowOutputProcessor()
        processor.schema = schema
        context = context or self._context_getter

        decoration = OutputHeadersControllerWrapper(
            context=context,
            processor=processor,
            error_http_code=error_http_code,
            default_http_code=default_http_code,
        )

        def decorator(func):
            self._buffer.output_headers = OutputHeadersDescription(decoration)
            return decoration.get_wrapper(func)
        return decorator

    def input_headers(
        self,
        schema: typing.Any,
        processor: ProcessorInterface = None,
        context: ContextInterface = None,
        error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
        default_http_code: HTTPStatus = HTTPStatus.OK,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        processor = processor or MarshmallowInputProcessor()
        processor.schema = schema
        context = context or self._context_getter

        decoration = InputHeadersControllerWrapper(
            context=context,
            processor=processor,
            error_http_code=error_http_code,
            default_http_code=default_http_code,
        )

        def decorator(func):
            self._buffer.input_headers = InputHeadersDescription(decoration)
            return decoration.get_wrapper(func)
        return decorator

    def input_path(
        self,
        schema: typing.Any,
        processor: ProcessorInterface = None,
        context: ContextInterface = None,
        error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
        default_http_code: HTTPStatus = HTTPStatus.OK,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        processor = processor or MarshmallowInputProcessor()
        processor.schema = schema
        context = context or self._context_getter

        decoration = InputPathControllerWrapper(
            context=context,
            processor=processor,
            error_http_code=error_http_code,
            default_http_code=default_http_code,
        )

        def decorator(func):
            self._buffer.input_path = InputPathDescription(decoration)
            return decoration.get_wrapper(func)
        return decorator

    def input_query(
        self,
        schema: typing.Any,
        processor: ProcessorInterface = None,
        context: ContextInterface = None,
        error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
        default_http_code: HTTPStatus = HTTPStatus.OK,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        processor = processor or MarshmallowInputProcessor()
        processor.schema = schema
        context = context or self._context_getter

        decoration = InputQueryControllerWrapper(
            context=context,
            processor=processor,
            error_http_code=error_http_code,
            default_http_code=default_http_code,
        )

        def decorator(func):
            self._buffer.input_query = InputQueryDescription(decoration)
            return decoration.get_wrapper(func)
        return decorator

    def input_body(
        self,
        schema: typing.Any,
        processor: ProcessorInterface = None,
        context: ContextInterface = None,
        error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
        default_http_code: HTTPStatus = HTTPStatus.OK,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        processor = processor or MarshmallowInputProcessor()
        processor.schema = schema
        context = context or self._context_getter

        decoration = InputBodyControllerWrapper(
            context=context,
            processor=processor,
            error_http_code=error_http_code,
            default_http_code=default_http_code,
        )

        def decorator(func):
            self._buffer.input_body = InputBodyDescription(decoration)
            return decoration.get_wrapper(func)
        return decorator

    def input_forms(
        self,
        schema: typing.Any,
        processor: ProcessorInterface=None,
        context: ContextInterface=None,
        error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
        default_http_code: HTTPStatus = HTTPStatus.OK,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        processor = processor or MarshmallowInputProcessor()
        processor.schema = schema
        context = context or self._context_getter

        decoration = InputBodyControllerWrapper(
            context=context,
            processor=processor,
            error_http_code=error_http_code,
            default_http_code=default_http_code,
        )

        def decorator(func):
            self._buffer.input_forms = InputFormsDescription(decoration)
            return decoration.get_wrapper(func)
        return decorator

    def handle_exception(
        self,
        handled_exception_class: typing.Type[Exception],
        http_code: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
        context: ContextInterface = None,
    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
        context = context or self._context_getter

        decoration = ExceptionHandlerControllerWrapper(
            handled_exception_class,
            context,
            # TODO BS 20171013: Permit schema overriding, see #15
            schema=_default_global_error_schema,
            http_code=http_code,
        )

        def decorator(func):
            self._buffer.errors.append(ErrorDescription(decoration))
            return decoration.get_wrapper(func)
        return decorator

    def generate_doc(self, app):
        # FIXME: j'ai du tricher avec app, see #11
        # FIXME @Damien bottle specific code ! see #11
        # rendre ca generique
        app = app or self._context.get_app()
        doc_generator = DocGenerator()
        return doc_generator.get_doc(self._controllers, app)