Repository: youknowone/ring

Issue:
dataclass coder
https://pypi.org/project/dataclasses-serialization/

Discussion:

Patch:
diff --git a/docs/coder.rst b/docs/coder.rst
index 6fa4fe5..c5f7f7e 100644
--- a/docs/coder.rst
+++ b/docs/coder.rst
@@ -45,6 +45,7 @@ modern Python world.
     ring.coder.bypass_coder
     ring.coder.JsonCoder
     ring.coder.pickle_coder
+    ring.coder.DataclassCoder
 
 :see: :mod:`ring.coder` for the module including pre-registered coders.
 
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index 43f1960..ddeb26a 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -3,7 +3,7 @@ Quickstart
 
 To start, remember the philosophy of **Ring** is a human-friendly high-level
 interface *with* transparent and concrete low-level access. You probably be
-able to access most of the levels of **Ring** you want.
+able to access most of the level of **Ring** you want.
 
 
 Installation
@@ -151,13 +151,12 @@ Let's consider using external cache storage instead of :class:`lru`.
 
 **Ring** includes common cache storage supports. `Memcached` is one of the
 popular cache storage. `Memcached` itself is out of the Python world. You must
-install and run it to let your python code connects there. Fortunately,
-because `Memcached` is very popular, it is well-packaged on most of the
-platforms. Check how to install it on your platform.
+install and run it to let your python code connects there. Because `Memcached`
+is very popular, it is well-packaged on most of the platforms. Check how to
+install it on your platform.
 
-:note: For example, ``apt install memcached`` for Debian/Ubuntu.
-    ``yum install memcached`` for CentOS/RHEL. ``brew install memcache`` for
-    macOS with Homebrew_.
+:note: ``apt install memcached`` for Debian/Ubuntu. ``yum install memcached``
+       for CentOS/RHEL ``brew install memcache`` for macOS with Homebrew_.
 
 Once you installed it, do not forget to start it.
 
diff --git a/ring/coder.py b/ring/coder.py
index beba8a0..da7e55a 100644
--- a/ring/coder.py
+++ b/ring/coder.py
@@ -7,6 +7,7 @@ stored cache data.
 import abc
 import six
 from collections import namedtuple
+
 try:
     import ujson as json_mod
 except ImportError:
@@ -17,6 +18,11 @@ try:
 except ImportError:
     import pickle as pickle_mod
 
+try:
+    import dataclasses
+except ImportError:
+    dataclasses = None
+
 
 @six.add_metaclass(abc.ABCMeta)
 class Coder(object):
@@ -63,7 +69,7 @@ class Registry(object):
     :see: :func:`ring.coder.registry` for default registry instance.
     """
 
-    __slots__ = ('coders', )
+    __slots__ = ('coders',)
 
     def __init__(self):
         self.coders = {}
@@ -113,7 +119,6 @@ def bypass(x):
 #: encode and decode functions bypass the given parameter.
 bypass_coder = bypass, bypass
 
-
 #: Pickle coder.
 #:
 #: encode is :func:`pickle.dumps` and decode is :func:`pickle.loads`.
@@ -139,6 +144,23 @@ class JsonCoder(Coder):
         return json_mod.loads(binary.decode('utf-8'))
 
 
+if dataclasses:
+    class DataclassCoder(Coder):
+
+        @staticmethod
+        def encode(data):
+            """Serialize dataclass object to json encoded dictionary"""
+            target_dict = (type(data).__name__, dataclasses.asdict(data))
+            return JsonCoder.encode(target_dict)
+
+        @staticmethod
+        def decode(binary):
+            """Deserialize json encoded dictionary to dataclass object"""
+            name, fields = JsonCoder.decode(binary)
+            dataclass = dataclasses.make_dataclass(name, [(key, type(value)) for key, value in fields.items()])
+            instance = dataclass(**fields)
+            return instance
+
 #: The default coder registry with pre-registered coders.
 #: Built-in coders are registered by default.
 #:
@@ -147,3 +169,6 @@ registry = Registry()
 registry.register(None, bypass_coder)
 registry.register('json', JsonCoder())
 registry.register('pickle', pickle_coder)
+
+if dataclasses:
+    registry.register('dataclass', DataclassCoder())


Test patch:
diff --git a/tests/test_coder.py b/tests/test_coder.py
index 5dd740e..f185c6f 100644
--- a/tests/test_coder.py
+++ b/tests/test_coder.py
@@ -1,3 +1,4 @@
+import sys
 
 import ring
 from ring.coder import (
@@ -82,6 +83,21 @@ def test_ring_bare_coder():
     assert f() == 10
 
 
+if sys.version_info >= (3, 7):
+    from tests._test_module_py37 import DataClass
+
+    def test_dataclass_coder():
+        coder = default_registry.get('dataclass')
+        dataclass = DataClass('name', 1, {'test': 1})
+        encoded_dataclass = coder.encode(dataclass)
+        assert b'["DataClass", {"name": "name", "my_int": 1, "my_dict": {"test": 1}}]' == encoded_dataclass
+        decoded_dataclass = coder.decode(encoded_dataclass)
+        assert 'DataClass' == type(decoded_dataclass).__name__
+        assert decoded_dataclass.name == 'name'
+        assert decoded_dataclass.my_int == 1
+        assert decoded_dataclass.my_dict == {'test': 1}
+
+
 def test_unexisting_coder():
     cache = {}