Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support custom JSON encoder/decoder in BpmnWorkflowSerializer #220

Merged
merged 3 commits into from
Sep 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions SpiffWorkflow/bpmn/serializer/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class BpmnWorkflowSerializer:
# using the configure_workflow_spec_converter.
VERSION = "1.0"
VERSION_KEY = "serializer_version"
JSON_ENCODER_CLS = None
JSON_DECODER_CLS = None

@staticmethod
def configure_workflow_spec_converter(task_spec_overrides=None, data_converter=None, version=VERSION):
Expand Down Expand Up @@ -97,17 +99,21 @@ def configure_workflow_spec_converter(task_spec_overrides=None, data_converter=N
return BpmnProcessSpecConverter(converters, version)


def __init__(self, spec_converter=None, data_converter=None, wf_class=None, version=VERSION):
def __init__(self, spec_converter=None, data_converter=None, wf_class=None, version=VERSION, json_encoder_cls=JSON_ENCODER_CLS, json_decoder_cls=JSON_DECODER_CLS):
"""Intializes a Workflow Serializer with the given Workflow, Task and Data Converters.

:param spec_converter: the workflow spec converter
:param data_converter: the data converter
:param wf_class: the workflow class
:param json_encoder_cls: JSON encoder class to be used for dumps/dump operations
:param json_decoder_cls: JSON decoder class to be used for loads/load operations
"""
super().__init__()
self.spec_converter = spec_converter if spec_converter is not None else self.configure_workflow_spec_converter()
self.data_converter = data_converter if data_converter is not None else BpmnDataConverter()
self.wf_class = wf_class if wf_class is not None else BpmnWorkflow
self.json_encoder_cls = json_encoder_cls
self.json_decoder_cls = json_decoder_cls
self.VERSION = version

def serialize_json(self, workflow, use_gzip=False):
Expand All @@ -120,16 +126,16 @@ def serialize_json(self, workflow, use_gzip=False):
"""
dct = self.workflow_to_dict(workflow)
dct[self.VERSION_KEY] = self.VERSION
json_str = json.dumps(dct)
json_str = json.dumps(dct, cls=self.json_encoder_cls)
return gzip.compress(json_str.encode('utf-8')) if use_gzip else json_str

def __get_dict(self, serialization, use_gzip=False):
if isinstance(serialization, dict):
dct = serialization
elif use_gzip:
dct = json.loads(gzip.decompress(serialization))
dct = json.loads(gzip.decompress(serialization), cls=self.json_decoder_cls)
else:
dct = json.loads(serialization)
dct = json.loads(serialization, cls=self.json_decoder_cls)
return dct

def deserialize_json(self, serialization, read_only=False, use_gzip=False):
Expand Down
50 changes: 50 additions & 0 deletions tests/SpiffWorkflow/bpmn/BpmnWorkflowSerializerTest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import unittest
import json
from uuid import uuid4, UUID

from SpiffWorkflow.task import TaskState
from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
Expand Down Expand Up @@ -57,6 +58,55 @@ def testSerializeToOldSerializerThenNewSerializer(self):
def testSerializeWorkflow(self):
serialized = self.serializer.serialize_json(self.workflow)
json.loads(serialized)

def testSerializeWorkflowCustomJSONEncoderDecoder(self):
class MyCls:
a = 1
def to_dict(self):
return {'a': 1, 'my_type': 'mycls'}

@classmethod
def from_dict(self, data):
return MyCls()

class MyJsonEncoder(json.JSONEncoder):
def default(self, z):
if isinstance(z, MyCls):
return z.to_dict()
return super().default(z)

class MyJsonDecoder(json.JSONDecoder):
classes = {'mycls': MyCls}

def __init__(self, *args, **kwargs):
super().__init__(object_hook=self.object_hook, *args, **kwargs)

def object_hook(self, z):
if 'my_type' in z and z['my_type'] in self.classes:
return self.classes[z['my_type']].from_dict(z)

return z

unserializable = MyCls()

a_task_spec = self.workflow.spec.task_specs[list(self.workflow.spec.task_specs)[0]]
a_task = self.workflow.get_tasks_from_spec_name(a_task_spec.name)[0]
a_task.data['jsonTest'] = unserializable

try:
self.assertRaises(TypeError, self.serializer.serialize_json, self.workflow)
wf_spec_converter = BpmnWorkflowSerializer.configure_workflow_spec_converter([TestUserTaskConverter])
custom_serializer = BpmnWorkflowSerializer(wf_spec_converter, version=self.SERIALIZER_VERSION,json_encoder_cls=MyJsonEncoder, json_decoder_cls=MyJsonDecoder)
serialized_workflow = custom_serializer.serialize_json(self.workflow)
finally:
a_task.data.pop('jsonTest',None)

serialized_task = [x for x in json.loads(serialized_workflow)['tasks'].values() if x['task_spec'] == a_task_spec.name][0]
self.assertEqual(serialized_task['data']['jsonTest'], {'a': 1, 'my_type': 'mycls'})

deserialized_workflow = custom_serializer.deserialize_json(serialized_workflow)
deserialized_task = deserialized_workflow.get_tasks_from_spec_name(a_task_spec.name)[0]
self.assertTrue(isinstance(deserialized_task.data['jsonTest'], MyCls))

def testDeserializeWorkflow(self):
self._compare_with_deserialized_copy(self.workflow)
Expand Down