Skip to content

Commit e2cf517

Browse files
authored
Merge pull request #6 from arminhammer/task-definition-order-patch
fix: add a custom deserializer to TaskDefinition
2 parents 295051a + bdc8f31 commit e2cf517

File tree

2 files changed

+289
-2
lines changed

2 files changed

+289
-2
lines changed

core/src/lib.rs

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod unit_tests {
77
use crate::models::workflow::*;
88
use crate::models::task::*;
99
use crate::models::map::*;
10+
use serde_json::json;
1011

1112
#[test]
1213
fn create_workflow() {
@@ -37,4 +38,203 @@ mod unit_tests {
3738
assert_eq!(workflow.document.summary, summary);
3839
}
3940

41+
#[test]
42+
fn test_for_loop_definition_each_field_deserialization() {
43+
// This test verifies that ForLoopDefinition correctly deserializes "each"
44+
let for_loop_json = serde_json::json!({
45+
"each": "item",
46+
"in": ".items"
47+
});
48+
49+
let result: Result<ForLoopDefinition, _> = serde_json::from_value(for_loop_json);
50+
51+
match result {
52+
Ok(for_loop) => {
53+
assert_eq!(for_loop.each, "item", "The 'each' field should be 'item'");
54+
assert_eq!(for_loop.in_, ".items", "The 'in' field should be '.items'");
55+
}
56+
Err(e) => {
57+
panic!(
58+
"Failed to deserialize ForLoopDefinition with 'each' field: {}",
59+
e
60+
);
61+
}
62+
}
63+
}
64+
65+
#[test]
66+
fn test_for_task_deserialization() {
67+
// This is a valid For task - it has a "for" field and a "do" field
68+
let for_task_json = json!({
69+
"for": {
70+
"each": "item",
71+
"in": ".items"
72+
},
73+
"do": [
74+
{
75+
"processItem": {
76+
"call": "processFunction",
77+
"with": {
78+
"item": "${ .item }"
79+
}
80+
}
81+
}
82+
]
83+
});
84+
85+
let result: Result<TaskDefinition, _> = serde_json::from_value(for_task_json.clone());
86+
87+
match result {
88+
Ok(TaskDefinition::For(for_def)) => {
89+
assert_eq!(for_def.for_.each, "item");
90+
assert_eq!(for_def.for_.in_, ".items");
91+
assert_eq!(for_def.do_.entries.len(), 1);
92+
let has_process_item = for_def
93+
.do_
94+
.entries
95+
.iter()
96+
.any(|entry| entry.contains_key("processItem"));
97+
assert!(
98+
has_process_item,
99+
"For task should contain processItem subtask"
100+
);
101+
}
102+
Ok(TaskDefinition::Do(_)) => {
103+
panic!("For task incorrectly deserialized as DoTaskDefinition");
104+
}
105+
Ok(other) => {
106+
panic!("For task deserialized as unexpected variant: {:?}", other);
107+
}
108+
Err(e) => {
109+
panic!("Failed to deserialize For task: {}", e);
110+
}
111+
}
112+
}
113+
114+
#[test]
115+
fn test_do_task_deserialization() {
116+
// This is a valid Do task
117+
let do_task_json = json!({
118+
"do": [
119+
{
120+
"step1": {
121+
"call": "function1"
122+
}
123+
},
124+
{
125+
"step2": {
126+
"call": "function2"
127+
}
128+
}
129+
]
130+
});
131+
132+
let result: Result<TaskDefinition, _> = serde_json::from_value(do_task_json);
133+
134+
match result {
135+
Ok(TaskDefinition::Do(do_def)) => {
136+
assert_eq!(do_def.do_.entries.len(), 2);
137+
let has_step1 = do_def
138+
.do_
139+
.entries
140+
.iter()
141+
.any(|entry| entry.contains_key("step1"));
142+
let has_step2 = do_def
143+
.do_
144+
.entries
145+
.iter()
146+
.any(|entry| entry.contains_key("step2"));
147+
assert!(has_step1, "Do task should contain step1");
148+
assert!(has_step2, "Do task should contain step2");
149+
}
150+
Ok(other) => {
151+
panic!("Do task deserialized as unexpected variant: {:?}", other);
152+
}
153+
Err(e) => {
154+
panic!("Failed to deserialize Do task: {}", e);
155+
}
156+
}
157+
}
158+
159+
#[test]
160+
fn test_for_task_with_while_condition() {
161+
// TestFor task with a while condition
162+
let for_task_json = json!({
163+
"for": {
164+
"each": "user",
165+
"in": ".users",
166+
"at": "index"
167+
},
168+
"while": "${ .index < 10 }",
169+
"do": [
170+
{
171+
"notifyUser": {
172+
"call": "notifyUser",
173+
"with": {
174+
"user": "${ .user }",
175+
"index": "${ .index }"
176+
}
177+
}
178+
}
179+
]
180+
});
181+
182+
let result: Result<TaskDefinition, _> = serde_json::from_value(for_task_json.clone());
183+
184+
match result {
185+
Ok(TaskDefinition::For(for_def)) => {
186+
assert_eq!(for_def.for_.each, "user");
187+
assert_eq!(for_def.for_.in_, ".users");
188+
assert_eq!(for_def.for_.at, Some("index".to_string()));
189+
assert_eq!(for_def.while_, Some("${ .index < 10 }".to_string()));
190+
assert_eq!(for_def.do_.entries.len(), 1);
191+
}
192+
Ok(TaskDefinition::Do(_)) => {
193+
panic!("For task incorrectly deserialized as DoTaskDefinition");
194+
}
195+
Ok(other) => {
196+
panic!("For task deserialized as unexpected variant: {:?}", other);
197+
}
198+
Err(e) => {
199+
panic!("Failed to deserialize For task with while: {}", e);
200+
}
201+
}
202+
}
203+
204+
#[test]
205+
fn test_roundtrip_serialization() {
206+
// Create a ForTaskDefinition programmatically
207+
208+
let for_loop = ForLoopDefinition::new("item", ".collection", None, None);
209+
let mut do_tasks = Map::new();
210+
do_tasks.add(
211+
"task1".to_string(),
212+
TaskDefinition::Call(CallTaskDefinition::new("someFunction", None, None)),
213+
);
214+
215+
let for_task = ForTaskDefinition::new(for_loop, do_tasks, None);
216+
let task_def = TaskDefinition::For(for_task);
217+
218+
// Serialize to JSON
219+
let json_str = serde_json::to_string(&task_def).expect("Failed to serialize");
220+
println!("Serialized: {}", json_str);
221+
222+
// Deserialize back
223+
let deserialized: TaskDefinition =
224+
serde_json::from_str(&json_str).expect("Failed to deserialize");
225+
226+
// Should still be a For task
227+
match deserialized {
228+
TaskDefinition::For(for_def) => {
229+
assert_eq!(for_def.for_.each, "item");
230+
assert_eq!(for_def.for_.in_, ".collection");
231+
}
232+
TaskDefinition::Do(_) => {
233+
panic!("After roundtrip serialization, For task became a Do task");
234+
}
235+
other => {
236+
panic!("Unexpected variant after roundtrip: {:?}", other);
237+
}
238+
}
239+
}
40240
}

core/src/models/task.rs

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ impl ProcessType {
5555
}
5656

5757
/// Represents a value that can be any of the supported task definitions
58-
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
58+
#[derive(Debug, Clone, PartialEq, Serialize)]
5959
#[serde(untagged)]
6060
pub enum TaskDefinition{
6161
/// Variant holding the definition of a 'call' task
@@ -84,6 +84,93 @@ pub enum TaskDefinition{
8484
Wait(WaitTaskDefinition)
8585
}
8686

87+
// Custom deserializer to handle For vs Do ambiguity
88+
impl<'de> serde::Deserialize<'de> for TaskDefinition {
89+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
90+
where
91+
D: serde::Deserializer<'de>,
92+
{
93+
let value = Value::deserialize(deserializer)?;
94+
95+
// Check for 'for' field first - if present, it's a For task
96+
if value.get("for").is_some() {
97+
return ForTaskDefinition::deserialize(value)
98+
.map(TaskDefinition::For)
99+
.map_err(serde::de::Error::custom);
100+
}
101+
102+
// Try other variants in priority order
103+
if value.get("call").is_some() {
104+
return CallTaskDefinition::deserialize(value)
105+
.map(TaskDefinition::Call)
106+
.map_err(serde::de::Error::custom);
107+
}
108+
109+
if value.get("set").is_some() {
110+
return SetTaskDefinition::deserialize(value)
111+
.map(TaskDefinition::Set)
112+
.map_err(serde::de::Error::custom);
113+
}
114+
115+
if value.get("fork").is_some() {
116+
return ForkTaskDefinition::deserialize(value)
117+
.map(TaskDefinition::Fork)
118+
.map_err(serde::de::Error::custom);
119+
}
120+
121+
if value.get("run").is_some() {
122+
return RunTaskDefinition::deserialize(value)
123+
.map(TaskDefinition::Run)
124+
.map_err(serde::de::Error::custom);
125+
}
126+
127+
if value.get("switch").is_some() {
128+
return SwitchTaskDefinition::deserialize(value)
129+
.map(TaskDefinition::Switch)
130+
.map_err(serde::de::Error::custom);
131+
}
132+
133+
if value.get("try").is_some() {
134+
return TryTaskDefinition::deserialize(value)
135+
.map(TaskDefinition::Try)
136+
.map_err(serde::de::Error::custom);
137+
}
138+
139+
if value.get("emit").is_some() {
140+
return EmitTaskDefinition::deserialize(value)
141+
.map(TaskDefinition::Emit)
142+
.map_err(serde::de::Error::custom);
143+
}
144+
145+
if value.get("raise").is_some() {
146+
return RaiseTaskDefinition::deserialize(value)
147+
.map(TaskDefinition::Raise)
148+
.map_err(serde::de::Error::custom);
149+
}
150+
151+
if value.get("wait").is_some() {
152+
return WaitTaskDefinition::deserialize(value)
153+
.map(TaskDefinition::Wait)
154+
.map_err(serde::de::Error::custom);
155+
}
156+
157+
if value.get("listen").is_some() {
158+
return ListenTaskDefinition::deserialize(value)
159+
.map(TaskDefinition::Listen)
160+
.map_err(serde::de::Error::custom);
161+
}
162+
163+
// If we get here and there's a 'do' field, it's a Do task (not a For task)
164+
if value.get("do").is_some() {
165+
return DoTaskDefinition::deserialize(value)
166+
.map(TaskDefinition::Do)
167+
.map_err(serde::de::Error::custom);
168+
}
169+
170+
Err(serde::de::Error::custom("unknown task type"))
171+
}
172+
}
173+
87174
/// A trait that all task definitions must implement
88175
pub trait TaskDefinitionBase {
89176
/// Gets the task's type
@@ -305,7 +392,7 @@ impl ForTaskDefinition {
305392
pub struct ForLoopDefinition{
306393

307394
/// Gets/sets the name of the variable that represents each element in the collection during iteration
308-
#[serde(rename = "emit")]
395+
#[serde(rename = "each")]
309396
pub each: String,
310397

311398
/// Gets/sets the runtime expression used to get the collection to iterate over

0 commit comments

Comments
 (0)