From 949c9aca7471f4b43f50605c4c5c8d712ef6a6a0 Mon Sep 17 00:00:00 2001 From: Erik Tuerke Date: Fri, 20 Mar 2020 08:23:05 +0100 Subject: [PATCH] pass Accept charset to the response serializer --- .../default_bytes_text_response_serializer.py | 13 +++++++---- .../default_dict_json_response_serializer.py | 10 ++++---- .../default_dict_text_response_serializer.py | 9 +++++--- .../default_str_text_response_serializer.py | 10 ++++---- .../dict_fallback_response_serializer.py | 8 +++++-- .../str_fallback_response_serializer.py | 11 +++++---- .../internal/response_serializer_service.py | 23 ++++++++++--------- restit/response_serializer.py | 22 ++++++++++-------- test/restit/open_api/open_api_spec_test.py | 2 +- test/restit/response_serializer_test.py | 12 ++++++---- 10 files changed, 73 insertions(+), 47 deletions(-) diff --git a/restit/internal/default_response_serializer/default_bytes_text_response_serializer.py b/restit/internal/default_response_serializer/default_bytes_text_response_serializer.py index b0f694e..e95da8f 100644 --- a/restit/internal/default_response_serializer/default_bytes_text_response_serializer.py +++ b/restit/internal/default_response_serializer/default_bytes_text_response_serializer.py @@ -1,9 +1,9 @@ from typing import List, Tuple, Union -from restit.common import guess_text_content_subtype_bytes, get_default_encoding +from restit.common import guess_text_content_subtype_bytes from restit.internal.response_status_parameter import ResponseStatusParameter from restit.internal.schema_or_field_deserializer import SchemaOrFieldDeserializer -from restit.response_serializer import ResponseSerializer +from restit.response_serializer import ResponseSerializer, CanHandleResultType class DefaultBytesTextResponseSerializer(ResponseSerializer): @@ -15,16 +15,19 @@ def get_response_data_type(self) -> type: return bytes def validate_and_serialize( - self, response_input: bytes, response_status_parameter: Union[None, ResponseStatusParameter] + self, + response_input: bytes, + response_status_parameter: Union[None, ResponseStatusParameter], + can_handle_result: CanHandleResultType ) -> Tuple[bytes, str]: content_type = guess_text_content_subtype_bytes(response_input) schema_or_field = self.find_schema(content_type, response_status_parameter) if schema_or_field: response_input = bytes( str(SchemaOrFieldDeserializer.deserialize( - response_input.decode(get_default_encoding()), schema_or_field + response_input.decode(can_handle_result.mime_type.charset), schema_or_field )), - encoding=get_default_encoding() + encoding=can_handle_result.mime_type.charset ) return response_input, content_type diff --git a/restit/internal/default_response_serializer/default_dict_json_response_serializer.py b/restit/internal/default_response_serializer/default_dict_json_response_serializer.py index 78ee832..db3ebdc 100644 --- a/restit/internal/default_response_serializer/default_dict_json_response_serializer.py +++ b/restit/internal/default_response_serializer/default_dict_json_response_serializer.py @@ -1,9 +1,8 @@ import json from typing import List, Tuple, Union -from restit.common import get_default_encoding from restit.internal.response_status_parameter import ResponseStatusParameter -from restit.response_serializer import ResponseSerializer +from restit.response_serializer import ResponseSerializer, CanHandleResultType class DefaultDictJsonResponseSerializer(ResponseSerializer): @@ -14,7 +13,10 @@ def get_response_data_type(self) -> type: return dict def validate_and_serialize( - self, response_input: dict, response_status_parameter: Union[None, ResponseStatusParameter] + self, + response_input: dict, + response_status_parameter: Union[None, ResponseStatusParameter], + can_handle_result: CanHandleResultType ) -> Tuple[bytes, str]: content_type = "application/json" schema = ResponseSerializer.find_schema(content_type, response_status_parameter) @@ -22,4 +24,4 @@ def validate_and_serialize( json_string = schema.dumps(response_input) else: json_string = json.dumps(response_input) - return json_string.encode(encoding=get_default_encoding()), content_type + return json_string.encode(encoding=can_handle_result.mime_type.charset), content_type diff --git a/restit/internal/default_response_serializer/default_dict_text_response_serializer.py b/restit/internal/default_response_serializer/default_dict_text_response_serializer.py index 81b1809..8a4043a 100644 --- a/restit/internal/default_response_serializer/default_dict_text_response_serializer.py +++ b/restit/internal/default_response_serializer/default_dict_text_response_serializer.py @@ -3,7 +3,7 @@ from restit.internal.default_response_serializer.default_dict_json_response_serializer import \ DefaultDictJsonResponseSerializer from restit.internal.response_status_parameter import ResponseStatusParameter -from restit.response_serializer import ResponseSerializer +from restit.response_serializer import ResponseSerializer, CanHandleResultType class DefaultDictTextResponseSerializer(ResponseSerializer): @@ -15,10 +15,13 @@ def get_response_data_type(self) -> type: return dict def validate_and_serialize( - self, response_input: dict, response_status_parameter: Union[None, ResponseStatusParameter] + self, + response_input: dict, + response_status_parameter: Union[None, ResponseStatusParameter], + can_handle_result: CanHandleResultType ) -> Tuple[bytes, str]: response_in_bytes, _ = DefaultDictJsonResponseSerializer().validate_and_serialize( - response_input, response_status_parameter + response_input, response_status_parameter, can_handle_result ) return response_in_bytes, "text/plain" diff --git a/restit/internal/default_response_serializer/default_str_text_response_serializer.py b/restit/internal/default_response_serializer/default_str_text_response_serializer.py index c002602..9778aad 100644 --- a/restit/internal/default_response_serializer/default_str_text_response_serializer.py +++ b/restit/internal/default_response_serializer/default_str_text_response_serializer.py @@ -1,9 +1,9 @@ from typing import List, Tuple, Union -from restit.common import get_default_encoding, guess_text_content_subtype_string +from restit.common import guess_text_content_subtype_string from restit.internal.response_status_parameter import ResponseStatusParameter from restit.internal.schema_or_field_deserializer import SchemaOrFieldDeserializer -from restit.response_serializer import ResponseSerializer +from restit.response_serializer import ResponseSerializer, CanHandleResultType class DefaultStrTextResponseSerializer(ResponseSerializer): @@ -14,14 +14,16 @@ def get_response_data_type(self) -> type: return str def validate_and_serialize( - self, response_input: str, response_status_parameter: Union[None, ResponseStatusParameter] + self, response_input: str, + response_status_parameter: Union[None, ResponseStatusParameter], + can_handle_result: CanHandleResultType ) -> Tuple[bytes, str]: content_type = guess_text_content_subtype_string(response_input) schema_or_field = self.find_schema(content_type, response_status_parameter) if schema_or_field: response_input = str(SchemaOrFieldDeserializer.deserialize(response_input, schema_or_field)) - return response_input.encode(encoding=get_default_encoding()), content_type + return response_input.encode(encoding=can_handle_result.mime_type.charset), content_type class SchemaNotSupportedForStringResponseBody(Exception): pass diff --git a/restit/internal/default_response_serializer/dict_fallback_response_serializer.py b/restit/internal/default_response_serializer/dict_fallback_response_serializer.py index 2606a35..5f315a8 100644 --- a/restit/internal/default_response_serializer/dict_fallback_response_serializer.py +++ b/restit/internal/default_response_serializer/dict_fallback_response_serializer.py @@ -2,6 +2,7 @@ from restit.internal.default_response_serializer.default_dict_json_response_serializer import \ DefaultDictJsonResponseSerializer +from restit.internal.mime_type import MIMEType from restit.internal.response_status_parameter import ResponseStatusParameter from restit.response_serializer import ResponseSerializer @@ -14,10 +15,13 @@ def get_response_data_type(self) -> type: return dict def validate_and_serialize( - self, response_input: dict, response_status_parameter: Union[None, ResponseStatusParameter] + self, + response_input: dict, + response_status_parameter: Union[None, ResponseStatusParameter], + can_handle_result: MIMEType ) -> Tuple[bytes, str]: response_in_bytes, _ = DefaultDictJsonResponseSerializer().validate_and_serialize( - response_input, response_status_parameter + response_input, response_status_parameter, can_handle_result ) return response_in_bytes, "application/json" diff --git a/restit/internal/default_response_serializer/str_fallback_response_serializer.py b/restit/internal/default_response_serializer/str_fallback_response_serializer.py index 7d1a168..61a557a 100644 --- a/restit/internal/default_response_serializer/str_fallback_response_serializer.py +++ b/restit/internal/default_response_serializer/str_fallback_response_serializer.py @@ -1,10 +1,10 @@ from typing import List, Tuple, Union -from restit.common import get_default_encoding, guess_text_content_subtype_string +from restit.common import guess_text_content_subtype_string from restit.internal.default_response_serializer.default_str_text_response_serializer import \ DefaultStrTextResponseSerializer from restit.internal.response_status_parameter import ResponseStatusParameter -from restit.response_serializer import ResponseSerializer +from restit.response_serializer import ResponseSerializer, CanHandleResultType class StringFallbackResponseSerializer(ResponseSerializer): @@ -15,10 +15,13 @@ def get_response_data_type(self) -> type: return str def validate_and_serialize( - self, response_input: str, response_status_parameter: Union[None, ResponseStatusParameter] + self, + response_input: str, + response_status_parameter: Union[None, ResponseStatusParameter], + can_handle_result: CanHandleResultType ) -> Tuple[bytes, str]: content_type = guess_text_content_subtype_string(response_input) - response_input_bytes = response_input.encode(encoding=get_default_encoding()) + response_input_bytes = response_input.encode(encoding=can_handle_result.mime_type.charset) if self.find_schema(content_type, response_status_parameter): raise DefaultStrTextResponseSerializer.SchemaNotSupportedForStringResponseBody() diff --git a/restit/internal/response_serializer_service.py b/restit/internal/response_serializer_service.py index 300797f..c65f960 100644 --- a/restit/internal/response_serializer_service.py +++ b/restit/internal/response_serializer_service.py @@ -1,4 +1,4 @@ -from typing import List, Union +from typing import List, Union, Tuple from restit.exception import NotAcceptable from restit.internal.default_response_serializer.default_bytes_text_response_serializer import \ @@ -15,7 +15,7 @@ from restit.internal.http_accept import HttpAccept from restit.internal.response_status_parameter import ResponseStatusParameter from restit.response import Response -from restit.response_serializer import ResponseSerializer +from restit.response_serializer import ResponseSerializer, CanHandleResultType _DEFAULT_RESPONSE_SERIALIZER = [ DefaultDictJsonResponseSerializer(), @@ -43,14 +43,15 @@ def restore_default_response_serializer(): ResponseSerializerService._RESPONSE_SERIALIZER = _DEFAULT_RESPONSE_SERIALIZER.copy() @staticmethod - def get_matching_response_serializer_for_media_type(http_accept: HttpAccept) -> List[ResponseSerializer]: - matching_response_serializer = [ - response_serializer - for response_serializer in ResponseSerializerService._RESPONSE_SERIALIZER - if response_serializer.can_handle_incoming_media_type(http_accept) - ] + def get_matching_response_serializer_for_media_type( + http_accept: HttpAccept) -> List[Tuple[ResponseSerializer, CanHandleResultType]]: + response_serializer_matches = [] + for response_serializer in ResponseSerializerService._RESPONSE_SERIALIZER: + can_handle_result = response_serializer.can_handle_incoming_media_type(http_accept) + if can_handle_result is not None: + response_serializer_matches.append((response_serializer, can_handle_result)) - return sorted(matching_response_serializer, key=lambda s: s.priority, reverse=True) + return sorted(response_serializer_matches, key=lambda c: c[1].mime_type.quality, reverse=True) @staticmethod def validate_and_serialize_response_body( @@ -63,10 +64,10 @@ def validate_and_serialize_response_body( if not matching_response_serializer_list: raise NotAcceptable() - for response_serializer in matching_response_serializer_list: + for response_serializer, can_handle_result in matching_response_serializer_list: if isinstance(response.response_body_input, response_serializer.get_response_data_type()): response.content, content_type = response_serializer.validate_and_serialize( - response.response_body_input, response_status_parameter + response.response_body_input, response_status_parameter, can_handle_result ) # Todo encoding from incoming accept charset response.text = response.content.decode() diff --git a/restit/response_serializer.py b/restit/response_serializer.py index 5cdf979..f16a5d0 100644 --- a/restit/response_serializer.py +++ b/restit/response_serializer.py @@ -1,22 +1,23 @@ -from typing import Any, List, Tuple, Union +from typing import Any, List, Tuple, Union, NamedTuple from marshmallow import Schema from marshmallow.fields import Field from restit.internal.http_accept import HttpAccept +from restit.internal.mime_type import MIMEType from restit.internal.response_status_parameter import ResponseStatusParameter -class ResponseSerializer: - def __init__(self): - self.priority = 0 +class CanHandleResultType(NamedTuple): + content_type: str + mime_type: MIMEType + - def can_handle_incoming_media_type(self, http_accept: HttpAccept) -> bool: +class ResponseSerializer: + def can_handle_incoming_media_type(self, http_accept: HttpAccept) -> Union[None, CanHandleResultType]: best_match = http_accept.get_best_match(self.get_media_type_strings()) if best_match is not None: - self.priority = best_match[1].quality - return True - return False + return CanHandleResultType(best_match[0], best_match[1]) def get_media_type_strings(self) -> List[str]: raise NotImplemented() @@ -25,7 +26,10 @@ def get_response_data_type(self) -> type: raise NotImplemented() def validate_and_serialize( - self, response_input: Any, response_status_parameter: Union[None, ResponseStatusParameter] + self, + response_input: Any, + response_status_parameter: Union[None, ResponseStatusParameter], + can_handle_result: CanHandleResultType ) -> Tuple[bytes, str]: """Returns a tuple of the serialized bytes and the content type""" raise NotImplemented() diff --git a/test/restit/open_api/open_api_spec_test.py b/test/restit/open_api/open_api_spec_test.py index 6f4a55b..155e521 100644 --- a/test/restit/open_api/open_api_spec_test.py +++ b/test/restit/open_api/open_api_spec_test.py @@ -17,7 +17,7 @@ class MyRequestBodySchema(Schema): """A bird with a flight speed exceeding that of an unladen swallow. """ - field1 = fields.String(required=True, validate=[Regexp("\w+")]) + field1 = fields.String(required=True, validate=[Regexp(r"\w+")]) field1.__doc__ = "Description for field1" field2 = fields.Integer(validate=[Range(min=1, max=100)]) diff --git a/test/restit/response_serializer_test.py b/test/restit/response_serializer_test.py index 70c44c9..bad55e1 100644 --- a/test/restit/response_serializer_test.py +++ b/test/restit/response_serializer_test.py @@ -16,7 +16,7 @@ from restit.internal.response_serializer_service import ResponseSerializerService from restit.internal.response_status_parameter import ResponseStatusParameter from restit.response import Response -from restit.response_serializer import ResponseSerializer +from restit.response_serializer import ResponseSerializer, CanHandleResultType class ResponseSerializerTestCase(unittest.TestCase): @@ -64,14 +64,18 @@ def get_media_type_strings(self) -> List[str]: return ["my/type"] def validate_and_serialize( - self, response_input: Any, response_status_parameter: Union[None, ResponseStatusParameter] + self, + response_input: Any, + response_status_parameter: Union[None, ResponseStatusParameter], + can_handle_result: CanHandleResultType ) -> Tuple[bytes, str]: - return "".join(reversed(response_input)).encode(), "my/type" + assert can_handle_result.mime_type.charset == "ascii" + return "".join(reversed(response_input)).encode(encoding=can_handle_result.mime_type.charset), "my/type" ResponseSerializerService.register_response_serializer(MyResponseSerializer()) response = Response("Test") ResponseSerializerService.validate_and_serialize_response_body( - response, HttpAccept.from_accept_string("my/type") + response, HttpAccept.from_accept_string("my/type; charset=ascii") ) self.assertEqual(b'tseT', response.content)