Skip to content

Commit ec32839

Browse files
authored
Add activity trigger encode/decode logic for durable function (#53)
* Add activity trigger encode/decode logic for durable function * Added test cases * Fix python linting * Added unit tests for activity trigger decode
1 parent e06dae8 commit ec32839

File tree

2 files changed

+164
-18
lines changed

2 files changed

+164
-18
lines changed

azure/functions/durable_functions.py

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,91 @@
1-
from typing import Any
1+
import typing
2+
import json
3+
24
from azure.functions import _durable_functions
35

46
from . import meta
57

68

79
# Durable Function Orchestration Trigger
810
class OrchestrationTriggerConverter(meta.InConverter,
11+
meta.OutConverter,
912
binding='orchestrationTrigger',
1013
trigger=True):
1114
@classmethod
1215
def check_input_type_annotation(cls, pytype):
1316
return issubclass(pytype, _durable_functions.OrchestrationContext)
1417

18+
@classmethod
19+
def check_output_type_annotation(cls, pytype):
20+
# Implicit output should accept any return type
21+
return True
22+
1523
@classmethod
1624
def decode(cls,
1725
data: meta.Datum, *,
1826
trigger_metadata) -> _durable_functions.OrchestrationContext:
1927
return _durable_functions.OrchestrationContext(data.value)
2028

29+
@classmethod
30+
def encode(cls, obj: typing.Any, *,
31+
expected_type: typing.Optional[type]) -> meta.Datum:
32+
# Durable function context should be a json
33+
return meta.Datum(type='json', value=obj)
34+
2135
@classmethod
2236
def has_implicit_output(cls) -> bool:
2337
return True
2438

2539

2640
# Durable Function Activity Trigger
2741
class ActivityTriggerConverter(meta.InConverter,
42+
meta.OutConverter,
2843
binding='activityTrigger',
2944
trigger=True):
3045
@classmethod
3146
def check_input_type_annotation(cls, pytype):
3247
# Activity Trigger's arguments should accept any types
3348
return True
3449

50+
@classmethod
51+
def check_output_type_annotation(cls, pytype):
52+
# The activity trigger should accept any JSON serializable types
53+
return True
54+
3555
@classmethod
3656
def decode(cls,
3757
data: meta.Datum, *,
38-
trigger_metadata) -> Any:
39-
if getattr(data, 'value', None) is not None:
40-
return data.value
58+
trigger_metadata) -> typing.Any:
59+
data_type = data.type
60+
61+
# Durable functions extension always returns a string of json
62+
# See durable functions library's call_activity_task docs
63+
if data_type == 'string' or data_type == 'json':
64+
try:
65+
result = json.loads(data.value)
66+
except json.JSONDecodeError:
67+
# String failover if the content is not json serializable
68+
result = data.value
69+
except Exception:
70+
raise ValueError(
71+
'activity trigger input must be a string or a '
72+
f'valid json serializable ({data.value})')
73+
else:
74+
raise NotImplementedError(
75+
f'unsupported activity trigger payload type: {data_type}')
76+
77+
return result
78+
79+
@classmethod
80+
def encode(cls, obj: typing.Any, *,
81+
expected_type: typing.Optional[type]) -> meta.Datum:
82+
try:
83+
result = json.dumps(obj)
84+
except TypeError:
85+
raise ValueError(
86+
f'activity trigger output must be json serializable ({obj})')
4187

42-
return data
88+
return meta.Datum(type='json', value=result)
4389

4490
@classmethod
4591
def has_implicit_output(cls) -> bool:

tests/test_durable_functions.py

Lines changed: 113 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,19 +75,119 @@ def test_orchestration_trigger_has_implicit_return(self):
7575
OrchestrationTriggerConverter.has_implicit_output()
7676
)
7777

78-
def test_activity_trigger_accepts_any_types(self):
79-
datum_set = {
80-
Datum('string', str),
81-
Datum(123, int),
82-
Datum(1234.56, float),
83-
Datum('string'.encode('utf-8'), bytes),
84-
Datum(Datum('{ "json": true }', str), Datum)
85-
}
86-
87-
for datum in datum_set:
88-
out = ActivityTriggerConverter.decode(datum, trigger_metadata=None)
89-
self.assertEqual(out, datum.value)
90-
self.assertEqual(type(out), datum.type)
78+
def test_activity_trigger_inputs(self):
79+
# Activity Trigger only accept string type from durable extensions
80+
# It will be JSON deserialized into expected data type
81+
data = [
82+
{
83+
'input': Datum('sample', 'string'),
84+
'expected_value': 'sample',
85+
'expected_type': str
86+
},
87+
{
88+
'input': Datum('123', 'string'),
89+
'expected_value': 123,
90+
'expected_type': int
91+
},
92+
{
93+
'input': Datum('1234.56', 'string'),
94+
'expected_value': 1234.56,
95+
'expected_type': float
96+
},
97+
{
98+
'input': Datum('[ "do", "re", "mi" ]', 'string'),
99+
'expected_value': ["do", "re", "mi"],
100+
'expected_type': list
101+
},
102+
{
103+
'input': Datum('{ "number": "42" }', 'string'),
104+
'expected_value': {"number": "42"},
105+
'expected_type': dict
106+
}
107+
]
108+
109+
for datum in data:
110+
decoded = ActivityTriggerConverter.decode(
111+
data=datum['input'],
112+
trigger_metadata=None)
113+
self.assertEqual(decoded, datum['expected_value'])
114+
self.assertEqual(type(decoded), datum['expected_type'])
115+
116+
def test_activity_trigger_encode(self):
117+
# Activity Trigger allow any JSON serializable as outputs
118+
# The return value will be carried back to the Orchestrator function
119+
data = [
120+
{
121+
'output': str('sample'),
122+
'expected_value': Datum('"sample"', 'json'),
123+
},
124+
{
125+
'output': int(123),
126+
'expected_value': Datum('123', 'json'),
127+
},
128+
{
129+
'output': float(1234.56),
130+
'expected_value': Datum('1234.56', 'json')
131+
},
132+
{
133+
'output': list(["do", "re", "mi"]),
134+
'expected_value': Datum('["do", "re", "mi"]', 'json')
135+
},
136+
{
137+
'output': dict({"number": "42"}),
138+
'expected_value': Datum('{"number": "42"}', 'json')
139+
}
140+
]
141+
142+
for datum in data:
143+
encoded = ActivityTriggerConverter.encode(
144+
obj=datum['output'],
145+
expected_type=type(datum['output']))
146+
self.assertEqual(encoded, datum['expected_value'])
147+
148+
def test_activity_trigger_decode(self):
149+
# Activity Trigger allow inputs to be any JSON serializables
150+
# The input values to the trigger should be passed into arguments
151+
data = [
152+
{
153+
'input': Datum('sample_string', 'string'),
154+
'expected_value': str('sample_string')
155+
},
156+
{
157+
'input': Datum('"sample_json_string"', 'json'),
158+
'expected_value': str('sample_json_string')
159+
},
160+
{
161+
'input': Datum('{ "invalid": "json"', 'json'),
162+
'expected_value': str('{ "invalid": "json"')
163+
},
164+
{
165+
'input': Datum('true', 'json'),
166+
'expected_value': bool(True),
167+
},
168+
{
169+
'input': Datum('123', 'json'),
170+
'expected_value': int(123),
171+
},
172+
{
173+
'input': Datum('1234.56', 'json'),
174+
'expected_value': float(1234.56)
175+
},
176+
{
177+
'input': Datum('["do", "re", "mi"]', 'json'),
178+
'expected_value': list(["do", "re", "mi"])
179+
},
180+
{
181+
'input': Datum('{"number": "42"}', 'json'),
182+
'expected_value': dict({"number": "42"})
183+
}
184+
]
185+
186+
for datum in data:
187+
decoded = ActivityTriggerConverter.decode(
188+
data=datum['input'],
189+
trigger_metadata=None)
190+
self.assertEqual(decoded, datum['expected_value'])
91191

92192
def test_activity_trigger_has_implicit_return(self):
93193
self.assertTrue(

0 commit comments

Comments
 (0)