Skip to content

Commit e6869b9

Browse files
author
Mate Zoltan
committed
Add serializer to response wrapper
It can be useful, when the response fully or partially need to be stored in a database in a JSON serialized form. From python 3.8, the `_unwrap` method could be replaced with using singledispatchmethod.
1 parent e79bfac commit e6869b9

File tree

2 files changed

+292
-0
lines changed

2 files changed

+292
-0
lines changed

checkout_sdk/checkout_response.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
from collections.abc import Iterable, Mapping
2+
from inspect import isfunction, ismethod
3+
4+
15
class ResponseWrapper:
26

37
def __init__(self, http_metadata=None, data=None):
@@ -21,3 +25,77 @@ def _wrap(self, value):
2125
@staticmethod
2226
def _is_collection(value):
2327
return isinstance(value, (tuple, list, set, frozenset))
28+
29+
def dict(self):
30+
"""
31+
Serializes the instance to a dictionary recursively.
32+
33+
The result shall only contain JSON compatible data structures.
34+
35+
Circular references shall be replaced with representations of appropriate
36+
JSON references (https://json-spec.readthedocs.io/reference.html).
37+
"""
38+
return self._unwrap_object(self, cache={}, path=['#'])
39+
40+
@staticmethod
41+
def _cache(method):
42+
def decorated_method(cls, data, cache, path):
43+
if id(data) in cache:
44+
for ref_path in cache[id(data)]:
45+
if path[:len(ref_path)] == ref_path:
46+
return {'$ref': '/'.join(ref_path)}
47+
48+
cache[id(data)].append(path)
49+
50+
else:
51+
cache[id(data)] = [path]
52+
53+
return method(cls, data, cache, path)
54+
55+
return decorated_method
56+
57+
@classmethod
58+
@_cache
59+
def _unwrap_object(cls, data, cache, path):
60+
return {
61+
key: cls._unwrap(attr, cache, path + [key])
62+
for key in dir(data)
63+
if not key.startswith('__')
64+
and not cls._is_function(attr := getattr(data, key))
65+
}
66+
67+
@classmethod
68+
def _unwrap(cls, data, cache, path):
69+
if isinstance(data, (str, int, float, bool, type(None))):
70+
return data
71+
72+
elif isinstance(data, Mapping):
73+
return cls._unwrap_mapping(data, cache, path)
74+
75+
elif isinstance(data, Iterable):
76+
return cls._unwrap_iterable(data, cache, path)
77+
78+
else:
79+
return cls._unwrap_object(data, cache, path)
80+
81+
@classmethod
82+
@_cache
83+
def _unwrap_mapping(cls, data: Mapping, cache, path):
84+
return {
85+
key: cls._unwrap(value, cache, path + [key])
86+
for key, value in data.items()
87+
if not cls._is_function(value)
88+
}
89+
90+
@classmethod
91+
@_cache
92+
def _unwrap_iterable(cls, data: Iterable, cache, path):
93+
return [
94+
cls._unwrap(value, cache, path + [str(idx)])
95+
for idx, value in enumerate(data)
96+
if not cls._is_function(value)
97+
]
98+
99+
@staticmethod
100+
def _is_function(data):
101+
return isfunction(data) or ismethod(data)

tests/checkout_response_test.py

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
from checkout_sdk.checkout_response import ResponseWrapper
2+
3+
4+
def test_serialize_atomic_types():
5+
unwrapped_data = {
6+
'str_attr': 'attr-value',
7+
'int_attr': 1234,
8+
'float_attr': 56.78,
9+
'bool_attr': True,
10+
'none_attr': None,
11+
}
12+
13+
wrapped_data = ResponseWrapper(data=unwrapped_data)
14+
15+
assert wrapped_data.dict() == unwrapped_data
16+
17+
18+
def test_serialize_object():
19+
class ObjAttr:
20+
str_attr_2 = 'attr-value-2'
21+
int_attr = 1234
22+
float_attr = 56.78
23+
bool_attr = True
24+
none_attr = None
25+
26+
def callable_attr(self):
27+
pass
28+
29+
wrapped_data = ResponseWrapper(
30+
data={
31+
'str_attr_1': 'attr-value-1',
32+
'obj_attr': ObjAttr(),
33+
}
34+
)
35+
36+
assert isinstance(wrapped_data.obj_attr, ObjAttr)
37+
38+
assert wrapped_data.dict() == {
39+
'str_attr_1': 'attr-value-1',
40+
'obj_attr': {
41+
'str_attr_2': 'attr-value-2',
42+
'int_attr': 1234,
43+
'float_attr': 56.78,
44+
'bool_attr': True,
45+
'none_attr': None,
46+
},
47+
}
48+
49+
50+
def test_serialize_nested_objects():
51+
unwrapped_data = {
52+
'str_attr_1': 'attr-value-1',
53+
'int_attr_1': 123,
54+
'float_attr_1': 89.1,
55+
56+
'obj_attr_1': {
57+
'str_attr_2': 'attr-value-2',
58+
'int_attr_2': 456,
59+
'float_attr_2': 91.2,
60+
61+
'obj_attr_2': {
62+
'str_attr_3': 'attr-value-3',
63+
'int_attr_3': 789,
64+
'float_attr_3': 12.3,
65+
}
66+
}
67+
}
68+
69+
wrapped_data = ResponseWrapper(data=unwrapped_data)
70+
71+
assert isinstance(wrapped_data.obj_attr_1, ResponseWrapper)
72+
assert isinstance(wrapped_data.obj_attr_1.obj_attr_2, ResponseWrapper)
73+
74+
assert wrapped_data.dict() == unwrapped_data
75+
76+
77+
def test_serialize_mapping():
78+
unwrapped_data = {
79+
'str_attr_1': 'attr-value-1',
80+
'int_attr_1': 123,
81+
'float_attr_1': 45.6,
82+
83+
'mapping_attr': {
84+
'str_attr_2': 'attr-value-2',
85+
'obj_attr': {
86+
'str_attr_3': 'attr-value-3'
87+
},
88+
},
89+
}
90+
91+
wrapped_data = ResponseWrapper(data=unwrapped_data)
92+
93+
assert isinstance(wrapped_data.mapping_attr.obj_attr, ResponseWrapper)
94+
95+
mapping = unwrapped_data['mapping_attr'].copy()
96+
mapping['obj_attr'] = wrapped_data.mapping_attr.obj_attr
97+
mapping['callable_attr'] = lambda: None
98+
wrapped_data.mapping_attr = mapping
99+
100+
assert wrapped_data.dict() == unwrapped_data
101+
102+
103+
def test_serialize_iterable():
104+
unwrapped_data = {
105+
'str_attr_1': 'attr-value-1',
106+
'int_attr_1': 123,
107+
'float_attr_1': 45.6,
108+
109+
'iterable_attr': [
110+
'list-item-1',
111+
{
112+
'str_attr_2': 'attr-value-2'
113+
},
114+
],
115+
}
116+
117+
wrapped_data = ResponseWrapper(data=unwrapped_data)
118+
119+
assert isinstance(wrapped_data.iterable_attr, list)
120+
assert isinstance(wrapped_data.iterable_attr[1], ResponseWrapper)
121+
122+
wrapped_data.iterable_attr.append(lambda: None)
123+
124+
assert wrapped_data.dict() == unwrapped_data
125+
126+
127+
def test_serialize_list_from_any_iterables():
128+
unwrapped_data = {
129+
'tuple_attr': (
130+
'tuple-item-1',
131+
'tuple-item-2',
132+
12.34,
133+
),
134+
'set_attr': {
135+
'set-item-1',
136+
'set-item-2',
137+
56.78,
138+
},
139+
}
140+
141+
wrapped_data = ResponseWrapper(data=unwrapped_data)
142+
143+
assert isinstance(wrapped_data.tuple_attr, tuple)
144+
assert isinstance(wrapped_data.set_attr, set)
145+
146+
serialized_data = wrapped_data.dict()
147+
148+
assert isinstance(serialized_data['tuple_attr'], list)
149+
assert isinstance(serialized_data['set_attr'], list)
150+
151+
assert tuple(serialized_data['tuple_attr']) == unwrapped_data['tuple_attr']
152+
assert set(serialized_data['set_attr']) == unwrapped_data['set_attr']
153+
154+
155+
def test_serialize_circular_references():
156+
unwrapped_data = {
157+
'str_attr': 'attr-value',
158+
'iterable_attr_1': [
159+
0,
160+
1,
161+
2,
162+
{
163+
'obj_attr': {
164+
'iterable_attr_2': [3, 4],
165+
},
166+
},
167+
],
168+
'iterable_attr_3': [5, 6],
169+
'iterable_attr_4': [7, 8],
170+
'iterable_attr_5': [9, 0],
171+
}
172+
unwrapped_data['iterable_attr_1'][3]['obj_attr']['circular_ref'] = \
173+
unwrapped_data['iterable_attr_1']
174+
175+
unwrapped_data['iterable_attr_1'][3]['obj_attr']['forward_ref'] = \
176+
unwrapped_data['iterable_attr_3']
177+
178+
unwrapped_data['iterable_attr_1'][3]['obj_attr']['iterable_attr_2'][1] = \
179+
unwrapped_data['iterable_attr_1'][3]
180+
181+
unwrapped_data['iterable_attr_4'][0] = unwrapped_data['iterable_attr_5']
182+
unwrapped_data['iterable_attr_5'][1] = unwrapped_data['iterable_attr_4']
183+
184+
wrapped_data = ResponseWrapper(http_metadata=unwrapped_data)
185+
186+
assert wrapped_data.dict() == {
187+
'http_metadata': {
188+
'str_attr': 'attr-value',
189+
'iterable_attr_1': [
190+
0,
191+
1,
192+
2,
193+
{
194+
'obj_attr': {
195+
'iterable_attr_2': [
196+
3,
197+
{'$ref': '#/http_metadata/iterable_attr_1/3'}
198+
],
199+
'circular_ref': {'$ref': '#/http_metadata/iterable_attr_1'},
200+
'forward_ref': [5, 6],
201+
},
202+
},
203+
],
204+
'iterable_attr_3': [5, 6],
205+
'iterable_attr_4': [
206+
[9, {'$ref': '#/http_metadata/iterable_attr_4'}],
207+
8,
208+
],
209+
'iterable_attr_5': [
210+
9,
211+
[{'$ref': '#/http_metadata/iterable_attr_5'}, 8],
212+
],
213+
},
214+
}

0 commit comments

Comments
 (0)