188 lines
5.1 KiB
Python
188 lines
5.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
# bthlabs-jsonrpc-core | (c) 2022-present Tomek Wójcik | MIT License
|
|
from __future__ import annotations
|
|
|
|
import datetime
|
|
import decimal
|
|
import typing
|
|
import uuid
|
|
|
|
from bthlabs_jsonrpc_core.exceptions import JSONRPCSerializerError
|
|
|
|
|
|
class JSONRPCSerializer:
|
|
"""
|
|
Serializer for JSONRPC responses.
|
|
|
|
This class is responsible for making the respones JSON-serializable.
|
|
Sequence types are all converted to lists. Dict-like types are all
|
|
converted to plain dicts. Simple types (``bool``, ``float``, ``int`` and
|
|
``str``) and ``None`` are returned as they are.
|
|
|
|
Datetime values are converted to strings using the ISO format. ``UUID`` and
|
|
``Decimal`` values are explicitly coerced to strings.
|
|
|
|
For values of other types, the serializer will try to invoke their
|
|
``to_rpc()`` method. If that fails, the serializer will raise
|
|
:py:exc:`JSONRPCSerializerError`.
|
|
|
|
Example:
|
|
|
|
.. code-block:: python
|
|
|
|
spam = ['eggs', {'spam': False}, Decimal('42.0')]
|
|
serializer = JSONRPCSerializer(spam)
|
|
print(serializer.data)
|
|
|
|
Example with ``to_rpc()``:
|
|
|
|
.. code-block:: python
|
|
|
|
class Spam:
|
|
def to_rpc(self):
|
|
return {
|
|
'spam': True
|
|
}
|
|
|
|
spam = ['eggs', Spam(), Decimal('42.0')]
|
|
serializer = JSONRPCSerializer(spam)
|
|
print(serializer.data)
|
|
"""
|
|
|
|
# Datetime types
|
|
DATETIME_TYPES = (datetime.date, datetime.datetime, datetime.time)
|
|
|
|
# Sequence types
|
|
SEQUENCE_TYPES = (set,)
|
|
|
|
# Simple types
|
|
SIMPLE_TYPES = (bool, float, int, str)
|
|
|
|
# Types that can be coerced to string
|
|
STRING_COERCIBLE_TYPES = (uuid.UUID, decimal.Decimal)
|
|
|
|
def __init__(self, data):
|
|
self._data = data
|
|
|
|
# pragma mark - Private interface
|
|
|
|
def is_simple_value(self, value: typing.Any) -> bool:
|
|
"""
|
|
Returns ``True`` if *value* is a simple value.
|
|
|
|
:meta private:
|
|
"""
|
|
value_type = type(value)
|
|
return (
|
|
value is None or value_type in self.SIMPLE_TYPES
|
|
)
|
|
|
|
def is_datetime_value(self, value: typing.Any) -> bool:
|
|
"""
|
|
Returns ``True`` if *value* is a datetime value.
|
|
|
|
:meta private:
|
|
"""
|
|
return type(value) in self.DATETIME_TYPES
|
|
|
|
def is_sequence_value(self, value: typing.Any) -> bool:
|
|
"""
|
|
Returns ``True`` if *value* is a sequence value.
|
|
|
|
:meta private:
|
|
"""
|
|
return any((
|
|
isinstance(value, typing.Sequence),
|
|
isinstance(value, typing.Generator),
|
|
type(value) in self.SEQUENCE_TYPES,
|
|
))
|
|
|
|
def is_dict_value(self, value: typing.Any) -> bool:
|
|
"""
|
|
Returns ``True`` if *value* is a simple value.
|
|
|
|
:meta private:
|
|
"""
|
|
return isinstance(value, typing.Dict)
|
|
|
|
def is_string_coercible_value(self, value: typing.Any) -> bool:
|
|
"""
|
|
Returns ``True`` if *value* is a coercible to string.
|
|
|
|
:meta private:
|
|
"""
|
|
return type(value) in self.STRING_COERCIBLE_TYPES
|
|
|
|
def serialize_datetime(self, value: typing.Any) -> typing.Any:
|
|
"""
|
|
Serializes a datetime value.
|
|
|
|
:meta private:
|
|
"""
|
|
return value.isoformat()
|
|
|
|
def serialize_sequence(self, value: typing.Any) -> typing.Any:
|
|
"""
|
|
Serializes a sequence value.
|
|
|
|
:meta private:
|
|
"""
|
|
return [self.serialize_value(item) for item in value]
|
|
|
|
def serialize_dict(self, value: typing.Any) -> typing.Any:
|
|
"""
|
|
Serializes a dict-like value.
|
|
|
|
:meta private:
|
|
"""
|
|
return {
|
|
key: self.serialize_value(item) for key, item in value.items()
|
|
}
|
|
|
|
def serialize_string_coercible(self, value: typing.Any) -> typing.Any:
|
|
"""
|
|
Serializes a string-coercible value.
|
|
|
|
:meta private:
|
|
"""
|
|
return str(value)
|
|
|
|
def serialize_value(self, value: typing.Any) -> typing.Any:
|
|
"""
|
|
Serializes *value* and returns the result.
|
|
|
|
:meta private:
|
|
"""
|
|
if isinstance(value, JSONRPCSerializer):
|
|
return value.data
|
|
elif self.is_simple_value(value):
|
|
return value
|
|
if self.is_datetime_value(value):
|
|
return self.serialize_datetime(value)
|
|
elif self.is_sequence_value(value):
|
|
return self.serialize_sequence(value)
|
|
elif self.is_dict_value(value):
|
|
return self.serialize_dict(value)
|
|
elif self.is_string_coercible_value(value):
|
|
return self.serialize_string_coercible(value)
|
|
elif hasattr(value, 'to_rpc'):
|
|
return self.serialize_value(value.to_rpc())
|
|
else:
|
|
raise JSONRPCSerializerError(
|
|
'Object of type {type} is not RPC serializable'.format(
|
|
type=type(value),
|
|
),
|
|
)
|
|
|
|
return value
|
|
|
|
# pragma mark - Public interface
|
|
|
|
@property
|
|
def data(self) -> typing.Any:
|
|
"""The serialized data."""
|
|
if not hasattr(self, '_serialized_data'):
|
|
self._serialized_data = self.serialize_value(self._data)
|
|
|
|
return self._serialized_data
|