zalando/connexion

View on GitHub
connexion/decorators/response.py

Summary

Maintainability
A
35 mins
Test Coverage
# Decorators to change the return type of endpoints
import functools
import logging

from jsonschema import ValidationError

from ..exceptions import (NonConformingResponseBody,
                          NonConformingResponseHeaders)
from ..utils import all_json, has_coroutine
from .decorator import BaseDecorator
from .validation import ResponseBodyValidator

logger = logging.getLogger('connexion.decorators.response')


class ResponseValidator(BaseDecorator):
    def __init__(self, operation, mimetype, validator=None):
        """
        :type operation: Operation
        :type mimetype: str
        :param validator: Validator class that should be used to validate passed data
                          against API schema.
        :type validator: jsonschema.IValidator
        """
        self.operation = operation
        self.mimetype = mimetype
        self.validator = validator

    def validate_response(self, data, status_code, headers, url):
        """
        Validates the Response object based on what has been declared in the specification.
        Ensures the response body matches the declated schema.
        :type data: dict
        :type status_code: int
        :type headers: dict
        :rtype bool | None
        """
        # check against returned header, fall back to expected mimetype
        content_type = headers.get("Content-Type", self.mimetype)
        content_type = content_type.rsplit(";", 1)[0]  # remove things like utf8 metadata

        response_definition = self.operation.response_definition(str(status_code), content_type)
        response_schema = self.operation.response_schema(str(status_code), content_type)

        if self.is_json_schema_compatible(response_schema):
            v = ResponseBodyValidator(response_schema, validator=self.validator)
            try:
                data = self.operation.json_loads(data)
                v.validate_schema(data, url)
            except ValidationError as e:
                raise NonConformingResponseBody(message=str(e))

        if response_definition and response_definition.get("headers"):
            # converting to set is needed to support python 2.7
            response_definition_header_keys = set(response_definition.get("headers").keys())
            header_keys = set(headers.keys())
            missing_keys = response_definition_header_keys - header_keys
            if missing_keys:
                pretty_list = ', '.join(missing_keys)
                msg = ("Keys in header don't match response specification. "
                       "Difference: {0}").format(pretty_list)
                raise NonConformingResponseHeaders(message=msg)
        return True

    def is_json_schema_compatible(self, response_schema):
        """
        Verify if the specified operation responses are JSON schema
        compatible.

        All operations that specify a JSON schema and have content
        type "application/json" or "text/plain" can be validated using
        json_schema package.

        :type response_schema: dict
        :rtype bool
        """
        if not response_schema:
            return False
        return all_json([self.mimetype]) or self.mimetype == 'text/plain'

    def __call__(self, function):
        """
        :type function: types.FunctionType
        :rtype: types.FunctionType
        """

        def _wrapper(request, response):
            connexion_response = \
                self.operation.api.get_connexion_response(response, self.mimetype)
            self.validate_response(
                connexion_response.body, connexion_response.status_code,
                connexion_response.headers, request.url)

            return response

        if has_coroutine(function):
            from .coroutine_wrappers import get_response_validator_wrapper
            wrapper = get_response_validator_wrapper(function, _wrapper)

        else:  # pragma: 3 no cover
            @functools.wraps(function)
            def wrapper(request):
                response = function(request)
                return _wrapper(request, response)

        return wrapper

    def __repr__(self):
        """
        :rtype: str
        """
        return '<ResponseValidator>'  # pragma: no cover