1"""JOSE interfaces.""" 2import abc 3import json 4from typing import Any 5 6from josepy import errors 7 8from collections.abc import Sequence, Mapping 9 10# pylint: disable=no-self-argument,no-method-argument,no-init,inherit-non-class 11# pylint: disable=too-few-public-methods 12 13 14class JSONDeSerializable(object, metaclass=abc.ABCMeta): 15 # pylint: disable=too-few-public-methods 16 """Interface for (de)serializable JSON objects. 17 18 Please recall, that standard Python library implements 19 :class:`json.JSONEncoder` and :class:`json.JSONDecoder` that perform 20 translations based on respective :ref:`conversion tables 21 <conversion-table>` that look pretty much like the one below (for 22 complete tables see relevant Python documentation): 23 24 .. _conversion-table: 25 26 ====== ====== 27 JSON Python 28 ====== ====== 29 object dict 30 ... ... 31 ====== ====== 32 33 While the above **conversion table** is about translation of JSON 34 documents to/from the basic Python types only, 35 :class:`JSONDeSerializable` introduces the following two concepts: 36 37 serialization 38 Turning an arbitrary Python object into Python object that can 39 be encoded into a JSON document. **Full serialization** produces 40 a Python object composed of only basic types as required by the 41 :ref:`conversion table <conversion-table>`. **Partial 42 serialization** (accomplished by :meth:`to_partial_json`) 43 produces a Python object that might also be built from other 44 :class:`JSONDeSerializable` objects. 45 46 deserialization 47 Turning a decoded Python object (necessarily one of the basic 48 types as required by the :ref:`conversion table 49 <conversion-table>`) into an arbitrary Python object. 50 51 Serialization produces **serialized object** ("partially serialized 52 object" or "fully serialized object" for partial and full 53 serialization respectively) and deserialization produces 54 **deserialized object**, both usually denoted in the source code as 55 ``jobj``. 56 57 Wording in the official Python documentation might be confusing 58 after reading the above, but in the light of those definitions, one 59 can view :meth:`json.JSONDecoder.decode` as decoder and 60 deserializer of basic types, :meth:`json.JSONEncoder.default` as 61 serializer of basic types, :meth:`json.JSONEncoder.encode` as 62 serializer and encoder of basic types. 63 64 One could extend :mod:`json` to support arbitrary object 65 (de)serialization either by: 66 67 - overriding :meth:`json.JSONDecoder.decode` and 68 :meth:`json.JSONEncoder.default` in subclasses 69 70 - or passing ``object_hook`` argument (or ``object_hook_pairs``) 71 to :func:`json.load`/:func:`json.loads` or ``default`` argument 72 for :func:`json.dump`/:func:`json.dumps`. 73 74 Interestingly, ``default`` is required to perform only partial 75 serialization, as :func:`json.dumps` applies ``default`` 76 recursively. This is the idea behind making :meth:`to_partial_json` 77 produce only partial serialization, while providing custom 78 :meth:`json_dumps` that dumps with ``default`` set to 79 :meth:`json_dump_default`. 80 81 To make further documentation a bit more concrete, please, consider 82 the following imaginatory implementation example:: 83 84 class Foo(JSONDeSerializable): 85 def to_partial_json(self): 86 return 'foo' 87 88 @classmethod 89 def from_json(cls, jobj): 90 return Foo() 91 92 class Bar(JSONDeSerializable): 93 def to_partial_json(self): 94 return [Foo(), Foo()] 95 96 @classmethod 97 def from_json(cls, jobj): 98 return Bar() 99 100 """ 101 102 @abc.abstractmethod 103 def to_partial_json(self) -> Any: # pragma: no cover 104 """Partially serialize. 105 106 Following the example, **partial serialization** means the following:: 107 108 assert isinstance(Bar().to_partial_json()[0], Foo) 109 assert isinstance(Bar().to_partial_json()[1], Foo) 110 111 # in particular... 112 assert Bar().to_partial_json() != ['foo', 'foo'] 113 114 :raises josepy.errors.SerializationError: 115 in case of any serialization error. 116 :returns: Partially serializable object. 117 118 """ 119 raise NotImplementedError() 120 121 def to_json(self) -> Any: 122 """Fully serialize. 123 124 Again, following the example from before, **full serialization** 125 means the following:: 126 127 assert Bar().to_json() == ['foo', 'foo'] 128 129 :raises josepy.errors.SerializationError: 130 in case of any serialization error. 131 :returns: Fully serialized object. 132 133 """ 134 def _serialize(obj: Any) -> Any: 135 if isinstance(obj, JSONDeSerializable): 136 return _serialize(obj.to_partial_json()) 137 if isinstance(obj, str): # strings are Sequence 138 return obj 139 elif isinstance(obj, list): 140 return [_serialize(subobj) for subobj in obj] 141 elif isinstance(obj, Sequence): 142 # default to tuple, otherwise Mapping could get 143 # unhashable list 144 return tuple(_serialize(subobj) for subobj in obj) 145 elif isinstance(obj, Mapping): 146 return {_serialize(key): _serialize(value) 147 for key, value in obj.items()} 148 else: 149 return obj 150 151 return _serialize(self) 152 153 @classmethod 154 @abc.abstractmethod 155 def from_json(cls, jobj: Any) -> 'JSONDeSerializable': 156 """Deserialize a decoded JSON document. 157 158 :param jobj: Python object, composed of only other basic data 159 types, as decoded from JSON document. Not necessarily 160 :class:`dict` (as decoded from "JSON object" document). 161 162 :raises josepy.errors.DeserializationError: 163 if decoding was unsuccessful, e.g. in case of unparseable 164 X509 certificate, or wrong padding in JOSE base64 encoded 165 string, etc. 166 167 """ 168 # TypeError: Can't instantiate abstract class <cls> with 169 # abstract methods from_json, to_partial_json 170 return cls() # pylint: disable=abstract-class-instantiated 171 172 @classmethod 173 def json_loads(cls, json_string: str) -> 'JSONDeSerializable': 174 """Deserialize from JSON document string.""" 175 try: 176 loads = json.loads(json_string) 177 except ValueError as error: 178 raise errors.DeserializationError(error) 179 return cls.from_json(loads) 180 181 def json_dumps(self, **kwargs: Any) -> str: 182 """Dump to JSON string using proper serializer. 183 184 :returns: JSON document string. 185 :rtype: str 186 187 """ 188 return json.dumps(self, default=self.json_dump_default, **kwargs) 189 190 def json_dumps_pretty(self) -> str: 191 """Dump the object to pretty JSON document string. 192 193 :rtype: str 194 195 """ 196 return self.json_dumps(sort_keys=True, indent=4, separators=(',', ': ')) 197 198 @classmethod 199 def json_dump_default(cls, python_object: 'JSONDeSerializable') -> Any: 200 """Serialize Python object. 201 202 This function is meant to be passed as ``default`` to 203 :func:`json.dump` or :func:`json.dumps`. They call 204 ``default(python_object)`` only for non-basic Python types, so 205 this function necessarily raises :class:`TypeError` if 206 ``python_object`` is not an instance of 207 :class:`IJSONSerializable`. 208 209 Please read the class docstring for more information. 210 211 """ 212 if isinstance(python_object, JSONDeSerializable): 213 return python_object.to_partial_json() 214 else: # this branch is necessary, cannot just "return" 215 raise TypeError(repr(python_object) + ' is not JSON serializable') 216