|
| 1 | +import json |
1 | 2 | import re |
2 | 3 | from typing import Union |
3 | 4 |
|
4 | | -from eth_utils import hexstr_if_str, to_hex, big_endian_to_int, int_to_big_endian |
| 5 | +from hexbytes import HexBytes |
| 6 | + |
| 7 | +from eth_utils import ( |
| 8 | + hexstr_if_str, |
| 9 | + to_hex, |
| 10 | + big_endian_to_int, |
| 11 | + int_to_big_endian |
| 12 | +) |
5 | 13 |
|
6 | 14 | from tronapi.utils.hexadecimal import ( |
7 | 15 | remove_0x_prefix, |
|
11 | 19 | ) |
12 | 20 |
|
13 | 21 | from tronapi.base.toolz import ( |
14 | | - curry, |
| 22 | + curry |
15 | 23 | ) |
16 | 24 |
|
17 | | -from tronapi.utils.types import is_boolean, is_integer |
| 25 | +from tronapi.utils.types import ( |
| 26 | + is_boolean, |
| 27 | + is_integer, |
| 28 | + is_list_like |
| 29 | +) |
| 30 | + |
| 31 | +from tronapi.base.datastructures import AttributeDict |
18 | 32 | from tronapi.utils.validation import assert_one_val |
19 | 33 |
|
20 | 34 |
|
@@ -129,3 +143,71 @@ def to_4byte_hex(hex_or_str_or_bytes: Union[int, str, bytes]) -> str: |
129 | 143 | ) |
130 | 144 | hex_str = encode_hex(byte_str) |
131 | 145 | return pad_hex(hex_str, size_of_4bytes) |
| 146 | + |
| 147 | + |
| 148 | +class FriendlyJsonSerialize: |
| 149 | + """ |
| 150 | + Friendly JSON serializer & deserializer |
| 151 | + When encoding or decoding fails, this class collects |
| 152 | + information on which fields failed, to show more |
| 153 | + helpful information in the raised error messages. |
| 154 | + """ |
| 155 | + |
| 156 | + def _json_mapping_errors(self, mapping): |
| 157 | + for key, val in mapping.items(): |
| 158 | + try: |
| 159 | + self._friendly_json_encode(val) |
| 160 | + except TypeError as exc: |
| 161 | + yield "%r: because (%s)" % (key, exc) |
| 162 | + |
| 163 | + def _json_list_errors(self, iterable): |
| 164 | + for index, element in enumerate(iterable): |
| 165 | + try: |
| 166 | + self._friendly_json_encode(element) |
| 167 | + except TypeError as exc: |
| 168 | + yield "%d: because (%s)" % (index, exc) |
| 169 | + |
| 170 | + def _friendly_json_encode(self, obj, cls=None): |
| 171 | + try: |
| 172 | + encoded = json.dumps(obj, cls=cls) |
| 173 | + return encoded |
| 174 | + except TypeError as full_exception: |
| 175 | + if hasattr(obj, 'items'): |
| 176 | + item_errors = '; '.join(self._json_mapping_errors(obj)) |
| 177 | + raise TypeError("dict had unencodable value at keys: {{{}}}".format(item_errors)) |
| 178 | + elif is_list_like(obj): |
| 179 | + element_errors = '; '.join(self._json_list_errors(obj)) |
| 180 | + raise TypeError("list had unencodable value at index: [{}]".format(element_errors)) |
| 181 | + else: |
| 182 | + raise full_exception |
| 183 | + |
| 184 | + @staticmethod |
| 185 | + def json_decode(json_str): |
| 186 | + try: |
| 187 | + decoded = json.loads(json_str) |
| 188 | + return decoded |
| 189 | + except json.decoder.JSONDecodeError as exc: |
| 190 | + err_msg = 'Could not decode {} because of {}.'.format(repr(json_str), exc) |
| 191 | + # Calling code may rely on catching JSONDecodeError to recognize bad json |
| 192 | + # so we have to re-raise the same type. |
| 193 | + raise json.decoder.JSONDecodeError(err_msg, exc.doc, exc.pos) |
| 194 | + |
| 195 | + def json_encode(self, obj, cls=None): |
| 196 | + try: |
| 197 | + return self._friendly_json_encode(obj, cls=cls) |
| 198 | + except TypeError as exc: |
| 199 | + raise TypeError("Could not encode to JSON: {}".format(exc)) |
| 200 | + |
| 201 | + |
| 202 | +class TronJsonEncoder(json.JSONEncoder): |
| 203 | + def default(self, obj): |
| 204 | + if isinstance(obj, AttributeDict): |
| 205 | + return {k: v for k, v in obj.items()} |
| 206 | + if isinstance(obj, HexBytes): |
| 207 | + return obj.hex() |
| 208 | + return json.JSONEncoder.default(self, obj) |
| 209 | + |
| 210 | + |
| 211 | +def to_json(obj: object) -> object: |
| 212 | + """Convert a complex object (like a transaction object) to a JSON string""" |
| 213 | + return FriendlyJsonSerialize().json_encode(obj, cls=TronJsonEncoder) |
0 commit comments