mirror of
https://github.com/correl/openapi-core.git
synced 2024-11-28 19:19:52 +00:00
Merge pull request #309 from p1c2u/feature/response-finder
Response finder
This commit is contained in:
commit
0865a4f54f
10 changed files with 88 additions and 66 deletions
|
@ -1,6 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""OpenAPI core operations models module"""
|
"""OpenAPI core operations models module"""
|
||||||
from openapi_core.schema.responses.exceptions import InvalidResponse
|
|
||||||
|
|
||||||
|
|
||||||
class Operation(object):
|
class Operation(object):
|
||||||
|
@ -29,18 +28,3 @@ class Operation(object):
|
||||||
|
|
||||||
def __getitem__(self, name):
|
def __getitem__(self, name):
|
||||||
return self.parameters[name]
|
return self.parameters[name]
|
||||||
|
|
||||||
def get_response(self, http_status='default'):
|
|
||||||
# @todo: move to Responses object
|
|
||||||
try:
|
|
||||||
return self.responses[http_status]
|
|
||||||
except KeyError:
|
|
||||||
# try range
|
|
||||||
http_status_range = '{0}XX'.format(http_status[0])
|
|
||||||
if http_status_range in self.responses:
|
|
||||||
return self.responses[http_status_range]
|
|
||||||
|
|
||||||
if 'default' not in self.responses:
|
|
||||||
raise InvalidResponse(http_status, self.responses)
|
|
||||||
|
|
||||||
return self.responses['default']
|
|
||||||
|
|
|
@ -7,16 +7,6 @@ class OpenAPIResponseError(OpenAPIMappingError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@attr.s(hash=True)
|
|
||||||
class InvalidResponse(OpenAPIResponseError):
|
|
||||||
http_status = attr.ib()
|
|
||||||
responses = attr.ib()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Unknown response http status: {0}".format(
|
|
||||||
str(self.http_status))
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s(hash=True)
|
@attr.s(hash=True)
|
||||||
class MissingResponseContent(OpenAPIResponseError):
|
class MissingResponseContent(OpenAPIResponseError):
|
||||||
response = attr.ib()
|
response = attr.ib()
|
||||||
|
|
0
openapi_core/templating/responses/__init__.py
Normal file
0
openapi_core/templating/responses/__init__.py
Normal file
18
openapi_core/templating/responses/exceptions.py
Normal file
18
openapi_core/templating/responses/exceptions.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import attr
|
||||||
|
|
||||||
|
from openapi_core.exceptions import OpenAPIError
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseFinderError(OpenAPIError):
|
||||||
|
"""Response finder error"""
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(hash=True)
|
||||||
|
class ResponseNotFound(ResponseFinderError):
|
||||||
|
"""Find response error"""
|
||||||
|
http_status = attr.ib()
|
||||||
|
responses = attr.ib()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Unknown response http status: {0}".format(
|
||||||
|
str(self.http_status))
|
23
openapi_core/templating/responses/finders.py
Normal file
23
openapi_core/templating/responses/finders.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
from openapi_core.templating.responses.exceptions import ResponseNotFound
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseFinder(object):
|
||||||
|
|
||||||
|
def __init__(self, responses):
|
||||||
|
self.responses = responses
|
||||||
|
|
||||||
|
def find(self, http_status='default'):
|
||||||
|
try:
|
||||||
|
return self.responses[http_status]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# try range
|
||||||
|
http_status_range = '{0}XX'.format(http_status[0])
|
||||||
|
if http_status_range in self.responses:
|
||||||
|
return self.responses[http_status_range]
|
||||||
|
|
||||||
|
if 'default' not in self.responses:
|
||||||
|
raise ResponseNotFound(http_status, self.responses)
|
||||||
|
|
||||||
|
return self.responses['default']
|
|
@ -1,11 +1,10 @@
|
||||||
"""OpenAPI core validation response validators module"""
|
"""OpenAPI core validation response validators module"""
|
||||||
from openapi_core.casting.schemas.exceptions import CastError
|
from openapi_core.casting.schemas.exceptions import CastError
|
||||||
from openapi_core.deserializing.exceptions import DeserializeError
|
from openapi_core.deserializing.exceptions import DeserializeError
|
||||||
from openapi_core.schema.responses.exceptions import (
|
from openapi_core.schema.responses.exceptions import MissingResponseContent
|
||||||
InvalidResponse, MissingResponseContent,
|
|
||||||
)
|
|
||||||
from openapi_core.templating.media_types.exceptions import MediaTypeFinderError
|
from openapi_core.templating.media_types.exceptions import MediaTypeFinderError
|
||||||
from openapi_core.templating.paths.exceptions import PathError
|
from openapi_core.templating.paths.exceptions import PathError
|
||||||
|
from openapi_core.templating.responses.exceptions import ResponseFinderError
|
||||||
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
|
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
|
||||||
from openapi_core.unmarshalling.schemas.exceptions import (
|
from openapi_core.unmarshalling.schemas.exceptions import (
|
||||||
UnmarshalError, ValidateError,
|
UnmarshalError, ValidateError,
|
||||||
|
@ -27,7 +26,7 @@ class ResponseValidator(BaseValidator):
|
||||||
operation_response = self._get_operation_response(
|
operation_response = self._get_operation_response(
|
||||||
operation, response)
|
operation, response)
|
||||||
# don't process if operation errors
|
# don't process if operation errors
|
||||||
except InvalidResponse as exc:
|
except ResponseFinderError as exc:
|
||||||
return ResponseValidationResult(errors=[exc, ])
|
return ResponseValidationResult(errors=[exc, ])
|
||||||
|
|
||||||
data, data_errors = self._get_data(response, operation_response)
|
data, data_errors = self._get_data(response, operation_response)
|
||||||
|
@ -43,7 +42,9 @@ class ResponseValidator(BaseValidator):
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_operation_response(self, operation, response):
|
def _get_operation_response(self, operation, response):
|
||||||
return operation.get_response(str(response.status_code))
|
from openapi_core.templating.responses.finders import ResponseFinder
|
||||||
|
finder = ResponseFinder(operation.responses)
|
||||||
|
return finder.find(str(response.status_code))
|
||||||
|
|
||||||
def _validate_data(self, request, response):
|
def _validate_data(self, request, response):
|
||||||
try:
|
try:
|
||||||
|
@ -56,7 +57,7 @@ class ResponseValidator(BaseValidator):
|
||||||
operation_response = self._get_operation_response(
|
operation_response = self._get_operation_response(
|
||||||
operation, response)
|
operation, response)
|
||||||
# don't process if operation errors
|
# don't process if operation errors
|
||||||
except InvalidResponse as exc:
|
except ResponseFinderError as exc:
|
||||||
return ResponseValidationResult(errors=[exc, ])
|
return ResponseValidationResult(errors=[exc, ])
|
||||||
|
|
||||||
data, data_errors = self._get_data(response, operation_response)
|
data, data_errors = self._get_data(response, operation_response)
|
||||||
|
|
|
@ -6,7 +6,7 @@ class TestLinkSpec(object):
|
||||||
def test_no_param(self, factory):
|
def test_no_param(self, factory):
|
||||||
spec_dict = factory.spec_from_file("data/v3.0/links.yaml")
|
spec_dict = factory.spec_from_file("data/v3.0/links.yaml")
|
||||||
spec = create_spec(spec_dict)
|
spec = create_spec(spec_dict)
|
||||||
resp = spec['/status']['get'].get_response()
|
resp = spec['/status']['get'].responses['default']
|
||||||
|
|
||||||
assert len(resp.links) == 1
|
assert len(resp.links) == 1
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ class TestLinkSpec(object):
|
||||||
def test_param(self, factory):
|
def test_param(self, factory):
|
||||||
spec_dict = factory.spec_from_file("data/v3.0/links.yaml")
|
spec_dict = factory.spec_from_file("data/v3.0/links.yaml")
|
||||||
spec = create_spec(spec_dict)
|
spec = create_spec(spec_dict)
|
||||||
resp = spec['/status/{resourceId}']['get'].get_response()
|
resp = spec['/status/{resourceId}']['get'].responses['default']
|
||||||
|
|
||||||
assert len(resp.links) == 1
|
assert len(resp.links) == 1
|
||||||
|
|
||||||
|
|
|
@ -8,14 +8,13 @@ from openapi_core.deserializing.exceptions import DeserializeError
|
||||||
from openapi_core.extensions.models.models import BaseModel
|
from openapi_core.extensions.models.models import BaseModel
|
||||||
from openapi_core.schema.parameters.exceptions import MissingRequiredParameter
|
from openapi_core.schema.parameters.exceptions import MissingRequiredParameter
|
||||||
from openapi_core.schema.request_bodies.exceptions import MissingRequestBody
|
from openapi_core.schema.request_bodies.exceptions import MissingRequestBody
|
||||||
from openapi_core.schema.responses.exceptions import (
|
from openapi_core.schema.responses.exceptions import MissingResponseContent
|
||||||
MissingResponseContent, InvalidResponse,
|
|
||||||
)
|
|
||||||
from openapi_core.shortcuts import create_spec
|
from openapi_core.shortcuts import create_spec
|
||||||
from openapi_core.templating.media_types.exceptions import MediaTypeNotFound
|
from openapi_core.templating.media_types.exceptions import MediaTypeNotFound
|
||||||
from openapi_core.templating.paths.exceptions import (
|
from openapi_core.templating.paths.exceptions import (
|
||||||
PathNotFound, OperationNotFound,
|
PathNotFound, OperationNotFound,
|
||||||
)
|
)
|
||||||
|
from openapi_core.templating.responses.exceptions import ResponseNotFound
|
||||||
from openapi_core.testing import MockRequest, MockResponse
|
from openapi_core.testing import MockRequest, MockResponse
|
||||||
from openapi_core.unmarshalling.schemas.exceptions import InvalidSchemaValue
|
from openapi_core.unmarshalling.schemas.exceptions import InvalidSchemaValue
|
||||||
from openapi_core.validation.exceptions import InvalidSecurity
|
from openapi_core.validation.exceptions import InvalidSecurity
|
||||||
|
@ -450,7 +449,7 @@ class TestResponseValidator(object):
|
||||||
result = validator.validate(request, response)
|
result = validator.validate(request, response)
|
||||||
|
|
||||||
assert len(result.errors) == 1
|
assert len(result.errors) == 1
|
||||||
assert type(result.errors[0]) == InvalidResponse
|
assert type(result.errors[0]) == ResponseNotFound
|
||||||
assert result.data is None
|
assert result.data is None
|
||||||
assert result.headers == {}
|
assert result.headers == {}
|
||||||
|
|
||||||
|
|
|
@ -17,31 +17,3 @@ class TestSchemas(object):
|
||||||
def test_iteritems(self, operation):
|
def test_iteritems(self, operation):
|
||||||
for name in operation.parameters:
|
for name in operation.parameters:
|
||||||
assert operation[name] == operation.parameters[name]
|
assert operation[name] == operation.parameters[name]
|
||||||
|
|
||||||
|
|
||||||
class TestResponses(object):
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def operation(self):
|
|
||||||
responses = {
|
|
||||||
'200': mock.sentinel.response_200,
|
|
||||||
'299': mock.sentinel.response_299,
|
|
||||||
'2XX': mock.sentinel.response_2XX,
|
|
||||||
'default': mock.sentinel.response_default,
|
|
||||||
}
|
|
||||||
return Operation('get', '/path', responses, parameters={})
|
|
||||||
|
|
||||||
def test_default(self, operation):
|
|
||||||
response = operation.get_response()
|
|
||||||
|
|
||||||
assert response == operation.responses['default']
|
|
||||||
|
|
||||||
def test_range(self, operation):
|
|
||||||
response = operation.get_response('201')
|
|
||||||
|
|
||||||
assert response == operation.responses['2XX']
|
|
||||||
|
|
||||||
def test_exact(self, operation):
|
|
||||||
response = operation.get_response('200')
|
|
||||||
|
|
||||||
assert response == operation.responses['200']
|
|
||||||
|
|
35
tests/unit/templating/test_responses_finders.py
Normal file
35
tests/unit/templating/test_responses_finders.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import mock
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from openapi_core.templating.responses.finders import ResponseFinder
|
||||||
|
|
||||||
|
|
||||||
|
class TestResponses(object):
|
||||||
|
|
||||||
|
@pytest.fixture(scope='class')
|
||||||
|
def responses(self):
|
||||||
|
return {
|
||||||
|
'200': mock.sentinel.response_200,
|
||||||
|
'299': mock.sentinel.response_299,
|
||||||
|
'2XX': mock.sentinel.response_2XX,
|
||||||
|
'default': mock.sentinel.response_default,
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.fixture(scope='class')
|
||||||
|
def finder(self, responses):
|
||||||
|
return ResponseFinder(responses)
|
||||||
|
|
||||||
|
def test_default(self, finder, responses):
|
||||||
|
response = finder.find()
|
||||||
|
|
||||||
|
assert response == responses['default']
|
||||||
|
|
||||||
|
def test_range(self, finder, responses):
|
||||||
|
response = finder.find('201')
|
||||||
|
|
||||||
|
assert response == responses['2XX']
|
||||||
|
|
||||||
|
def test_exact(self, finder, responses):
|
||||||
|
response = finder.find('200')
|
||||||
|
|
||||||
|
assert response == responses['200']
|
Loading…
Reference in a new issue