Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add serializer to response wrapper #145

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions checkout_sdk/checkout_response.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from collections.abc import Iterable, Mapping
from inspect import isfunction, ismethod


class ResponseWrapper:

def __init__(self, http_metadata=None, data=None):
Expand All @@ -21,3 +25,77 @@ def _wrap(self, value):
@staticmethod
def _is_collection(value):
return isinstance(value, (tuple, list, set, frozenset))

def dict(self):
"""
Serializes the instance to a dictionary recursively.

The result shall only contain JSON compatible data structures.

Circular references shall be replaced with representations of appropriate
JSON references (https://json-spec.readthedocs.io/reference.html).
"""
return self._unwrap_object(self, paths_by_id={}, path=['#'])

@staticmethod
def _handle_circular_ref(method):
def decorated_method(cls, data, paths_by_id, path):
if id(data) in paths_by_id:
for ref_path in paths_by_id[id(data)]:
if path[:len(ref_path)] == ref_path:
return {'$ref': '/'.join(ref_path)}

paths_by_id[id(data)].append(path)

else:
paths_by_id[id(data)] = [path]

return method(cls, data, paths_by_id, path)

return decorated_method

@classmethod
@_handle_circular_ref
def _unwrap_object(cls, data, paths_by_id, path):
return {
key: cls._unwrap(getattr(data, key), paths_by_id, path + [key])
for key in dir(data)
if not key.startswith('__')
and not cls._is_function(getattr(data, key))
}

@classmethod
def _unwrap(cls, data, paths_by_id, path):
if isinstance(data, (str, int, float, bool, type(None))):
return data

elif isinstance(data, Mapping):
return cls._unwrap_mapping(data, paths_by_id, path)

elif isinstance(data, Iterable):
return cls._unwrap_iterable(data, paths_by_id, path)

else:
return cls._unwrap_object(data, paths_by_id, path)

@classmethod
@_handle_circular_ref
def _unwrap_mapping(cls, data: Mapping, paths_by_id, path):
return {
key: cls._unwrap(value, paths_by_id, path + [key])
for key, value in data.items()
if not cls._is_function(value)
}

@classmethod
@_handle_circular_ref
def _unwrap_iterable(cls, data: Iterable, paths_by_id, path):
return [
cls._unwrap(value, paths_by_id, path + [str(idx)])
for idx, value in enumerate(data)
if not cls._is_function(value)
]

@staticmethod
def _is_function(data):
return isfunction(data) or ismethod(data)
214 changes: 214 additions & 0 deletions tests/checkout_response_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
from checkout_sdk.checkout_response import ResponseWrapper


def test_serialize_atomic_types():
unwrapped_data = {
'str_attr': 'attr-value',
'int_attr': 1234,
'float_attr': 56.78,
'bool_attr': True,
'none_attr': None,
}

wrapped_data = ResponseWrapper(data=unwrapped_data)

assert wrapped_data.dict() == unwrapped_data


def test_serialize_object():
class ObjAttr:
str_attr_2 = 'attr-value-2'
int_attr = 1234
float_attr = 56.78
bool_attr = True
none_attr = None

def callable_attr(self):
pass

wrapped_data = ResponseWrapper(
data={
'str_attr_1': 'attr-value-1',
'obj_attr': ObjAttr(),
}
)

assert isinstance(wrapped_data.obj_attr, ObjAttr)

assert wrapped_data.dict() == {
'str_attr_1': 'attr-value-1',
'obj_attr': {
'str_attr_2': 'attr-value-2',
'int_attr': 1234,
'float_attr': 56.78,
'bool_attr': True,
'none_attr': None,
},
}


def test_serialize_nested_objects():
unwrapped_data = {
'str_attr_1': 'attr-value-1',
'int_attr_1': 123,
'float_attr_1': 89.1,

'obj_attr_1': {
'str_attr_2': 'attr-value-2',
'int_attr_2': 456,
'float_attr_2': 91.2,

'obj_attr_2': {
'str_attr_3': 'attr-value-3',
'int_attr_3': 789,
'float_attr_3': 12.3,
}
}
}

wrapped_data = ResponseWrapper(data=unwrapped_data)

assert isinstance(wrapped_data.obj_attr_1, ResponseWrapper)
assert isinstance(wrapped_data.obj_attr_1.obj_attr_2, ResponseWrapper)

assert wrapped_data.dict() == unwrapped_data


def test_serialize_mapping():
unwrapped_data = {
'str_attr_1': 'attr-value-1',
'int_attr_1': 123,
'float_attr_1': 45.6,

'mapping_attr': {
'str_attr_2': 'attr-value-2',
'obj_attr': {
'str_attr_3': 'attr-value-3'
},
},
}

wrapped_data = ResponseWrapper(data=unwrapped_data)

assert isinstance(wrapped_data.mapping_attr.obj_attr, ResponseWrapper)

mapping = unwrapped_data['mapping_attr'].copy()
mapping['obj_attr'] = wrapped_data.mapping_attr.obj_attr
mapping['callable_attr'] = lambda: None
wrapped_data.mapping_attr = mapping

assert wrapped_data.dict() == unwrapped_data


def test_serialize_iterable():
unwrapped_data = {
'str_attr_1': 'attr-value-1',
'int_attr_1': 123,
'float_attr_1': 45.6,

'iterable_attr': [
'list-item-1',
{
'str_attr_2': 'attr-value-2'
},
],
}

wrapped_data = ResponseWrapper(data=unwrapped_data)

assert isinstance(wrapped_data.iterable_attr, list)
assert isinstance(wrapped_data.iterable_attr[1], ResponseWrapper)

wrapped_data.iterable_attr.append(lambda: None)

assert wrapped_data.dict() == unwrapped_data


def test_serialize_list_from_any_iterables():
unwrapped_data = {
'tuple_attr': (
'tuple-item-1',
'tuple-item-2',
12.34,
),
'set_attr': {
'set-item-1',
'set-item-2',
56.78,
},
}

wrapped_data = ResponseWrapper(data=unwrapped_data)

assert isinstance(wrapped_data.tuple_attr, tuple)
assert isinstance(wrapped_data.set_attr, set)

serialized_data = wrapped_data.dict()

assert isinstance(serialized_data['tuple_attr'], list)
assert isinstance(serialized_data['set_attr'], list)

assert tuple(serialized_data['tuple_attr']) == unwrapped_data['tuple_attr']
assert set(serialized_data['set_attr']) == unwrapped_data['set_attr']


def test_serialize_circular_references():
unwrapped_data = {
'str_attr': 'attr-value',
'iterable_attr_1': [
0,
1,
2,
{
'obj_attr': {
'iterable_attr_2': [3, 4],
},
},
],
'iterable_attr_3': [5, 6],
'iterable_attr_4': [7, 8],
'iterable_attr_5': [9, 0],
}
unwrapped_data['iterable_attr_1'][3]['obj_attr']['circular_ref'] = \
unwrapped_data['iterable_attr_1']

unwrapped_data['iterable_attr_1'][3]['obj_attr']['forward_ref'] = \
unwrapped_data['iterable_attr_3']

unwrapped_data['iterable_attr_1'][3]['obj_attr']['iterable_attr_2'][1] = \
unwrapped_data['iterable_attr_1'][3]

unwrapped_data['iterable_attr_4'][0] = unwrapped_data['iterable_attr_5']
unwrapped_data['iterable_attr_5'][1] = unwrapped_data['iterable_attr_4']

wrapped_data = ResponseWrapper(http_metadata=unwrapped_data)

assert wrapped_data.dict() == {
'http_metadata': {
'str_attr': 'attr-value',
'iterable_attr_1': [
0,
1,
2,
{
'obj_attr': {
'iterable_attr_2': [
3,
{'$ref': '#/http_metadata/iterable_attr_1/3'}
],
'circular_ref': {'$ref': '#/http_metadata/iterable_attr_1'},
'forward_ref': [5, 6],
},
},
],
'iterable_attr_3': [5, 6],
'iterable_attr_4': [
[9, {'$ref': '#/http_metadata/iterable_attr_4'}],
8,
],
'iterable_attr_5': [
9,
[{'$ref': '#/http_metadata/iterable_attr_5'}, 8],
],
},
}