Skip to content

Commit

Permalink
✨ Open up serialization to reduce clients from depending on jsons (#156)
Browse files Browse the repository at this point in the history
* ✨Open serialization methods to ensure consumers don't need jsons

Internal serialization was not fully exposed. This prevented for instance to get objects that would normally be returned in an API. This ensures API's will not have to implement jsons.

Added additional logic to handle keys that were getting placed within the deserialization.

* 🆙 Raise version to 1.1.3
  • Loading branch information
keithrfung authored Sep 1, 2020
1 parent a8de188 commit 48ed717
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 26 deletions.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
LONG_DESCRIPTION = readme_file.read()

NAME = "electionguard"
VERSION = "1.1.2"
VERSION = "1.1.3"
LICENSE = "MIT"
DESCRIPTION = "ElectionGuard: Support for e2e verified elections."
LONG_DESCRIPTION_CONTENT_TYPE = "text/markdown"
Expand Down
129 changes: 106 additions & 23 deletions src/electionguard/serializable.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from dataclasses import dataclass
from datetime import datetime
from os import path
from typing import cast, TypeVar, Generic
from typing import Any, cast, TypeVar, Generic

from jsons import (
dump,
dumps,
NoneType,
load,
loads,
JsonsError,
set_deserializer,
Expand All @@ -22,7 +24,7 @@
READ: str = "r"
JSON_PARSE_ERROR = '{"error": "Object could not be parsed due to json issue"}'
# TODO Issue #??: Jsons library incorrectly dumps class method
FROM_JSON_FILE = '"from_json_file": {}, '
KEYS_TO_REMOVE = ["from_json", "from_json_file", "from_json_object"]


@dataclass
Expand All @@ -33,18 +35,19 @@ class Serializable(Generic[T]):

def to_json(self, strip_privates: bool = True) -> str:
"""
Serialize to json
Serialize to json string
:param strip_privates: strip private variables
:return: the json string representation of this object
"""
return write_json(self, strip_privates)

def to_json_object(self, strip_privates: bool = True) -> Any:
"""
Serialize to json object
:param strip_privates: strip private variables
:return: the json representation of this object
"""
set_serializers()
suppress_warnings()
try:
return cast(
str, dumps(self, strip_privates=strip_privates, strip_nulls=True)
).replace(FROM_JSON_FILE, "")
except JsonsError:
return JSON_PARSE_ERROR
return write_json_object(self, strip_privates)

def to_json_file(
self, file_name: str, file_path: str = "", strip_privates: bool = True
Expand All @@ -55,35 +58,115 @@ def to_json_file(
:param file_path: File path
:param strip_privates: Strip private variables
"""
write_json_file(self.to_json(strip_privates), file_name, file_path)
write_json_file(self, file_name, file_path, strip_privates)

@classmethod
def from_json(cls, data: str) -> T:
"""
Deserialize the provided data string into the specified instance
:param data: JSON string
"""
set_deserializers()
return cast(T, loads(data, cls))

@classmethod
def from_json_object(cls, data: object) -> T:
"""
Deserialize the provided data object into the specified instance
:param data: JSON object
"""
set_deserializers()
return cast(T, load(data, cls))

@classmethod
def from_json_file(cls, file_name: str, file_path: str = "") -> T:
"""
Deserialize the provided file into the specified instance
:param file_name: File name
:param file_path: File path
"""
json_file_path: str = path.join(file_path, file_name + JSON_FILE_EXTENSION)
with open(json_file_path, READ) as json_file:
data = json_file.read()
target = cls.from_json(data)
return target

@classmethod
def from_json(cls, data: str) -> T:
"""
Deserialize the provided data string into the specified instance
"""
set_deserializers()
return cast(T, loads(data, cls))


def write_json_file(json_data: str, file_name: str, file_path: str = "") -> None:
def _remove_key(obj: Any, key_to_remove: str) -> Any:
"""
Remove key from object recursively
:param obj: Any object
:param key_to_remove: key to remove
"""
if isinstance(obj, dict):
for key in list(obj.keys()):
if key == key_to_remove:
del obj[key]
else:
_remove_key(obj[key], key_to_remove)
elif isinstance(obj, list):
for i in reversed(range(len(obj))):
if obj[i] == key_to_remove:
del obj[i]
else:
_remove_key(obj[i], key_to_remove)


def write_json(object_to_write: object, strip_privates: bool = True) -> str:
"""
Serialize to json string
:param object_to_write: object to write to json
:param strip_privates: strip private variables
:return: the json string representation of this object
"""
set_serializers()
suppress_warnings()
try:
json_object = write_json_object(object_to_write, strip_privates)
json_string = cast(
str, dumps(json_object, strip_privates=strip_privates, strip_nulls=True)
)
return json_string
except JsonsError:
return JSON_PARSE_ERROR


def write_json_object(object_to_write: object, strip_privates: bool = True) -> object:
"""
Serialize to json object
:param object_to_write: object to write to json
:param strip_privates: strip private variables
:return: the json representation of this object
"""
set_serializers()
suppress_warnings()
try:
json_object = dump(
object_to_write, strip_privates=strip_privates, strip_nulls=True
)
for key in KEYS_TO_REMOVE:
_remove_key(json_object, key)
return json_object
except JsonsError:
return JSON_PARSE_ERROR


def write_json_file(
object_to_write: object,
file_name: str,
file_path: str = "",
strip_privates: bool = True,
) -> None:
"""
Write json data string to json file
Serialize json data string to json file
:param object_to_write: object to write to json
:param file_name: File name
:param file_path: File path
:param strip_privates: strip private variables
"""
json_file_path: str = path.join(file_path, file_name + JSON_FILE_EXTENSION)
with open(json_file_path, WRITE) as json_file:
json_file.write(json_data)
json_file.write(write_json(object_to_write, strip_privates))


def set_serializers() -> None:
Expand Down
52 changes: 50 additions & 2 deletions tests/test_serializable.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,61 @@
set_deserializers,
set_serializers,
write_json_file,
write_json_object,
write_json,
)


class TestSerializable(TestCase):
def test_write_json(self) -> None:
# Arrange
json_data = {
"from_json_file": {},
"test": 1,
"nested": {"from_json_file": {}, "test": 1},
"array": [{"from_json_file": {}, "test": 1}],
}
expected_json_string = (
'{"test": 1, "nested": {"test": 1}, "array": [{"test": 1}]}'
)

# Act
json_string = write_json(json_data)

# Assert
self.assertEqual(json_string, expected_json_string)

def test_write_json_object(self) -> None:
# Arrange
json_data = {
"from_json_file": {},
"test": 1,
"nested": {"from_json_file": {}, "test": 1},
"array": [{"from_json_file": {}, "test": 1}],
}
expected_json_object = {
"test": 1,
"nested": {"test": 1},
"array": [{"test": 1}],
}

# Act
json_object = write_json_object(json_data)

# Assert
self.assertEqual(json_object, expected_json_object)

def test_write_json_file(self) -> None:
# Arrange
json_data = '{ "test" : 1 }'
json_data = {
"from_json_file": {},
"test": 1,
"nested": {"from_json_file": {}, "test": 1},
"array": [{"from_json_file": {}, "test": 1}],
}
expected_json_data = (
'{"test": 1, "nested": {"test": 1}, "array": [{"test": 1}]}'
)
file_name = "json_write_test"
json_file = file_name + ".json"

Expand All @@ -20,7 +68,7 @@ def test_write_json_file(self) -> None:

# Assert
with open(json_file) as reader:
self.assertEqual(reader.read(), json_data)
self.assertEqual(reader.read(), expected_json_data)

# Cleanup
remove(json_file)
Expand Down

0 comments on commit 48ed717

Please sign in to comment.