Skip to main content

swf_core/models/
workflow.rs

1use crate::models::authentication::*;
2use crate::models::catalog::*;
3use crate::models::duration::*;
4use crate::models::error::*;
5use crate::models::event::*;
6use crate::models::extension::*;
7use crate::models::input::*;
8use crate::models::map::*;
9use crate::models::output::*;
10use crate::models::retry::*;
11use crate::models::schema::SchemaDefinition;
12use crate::models::task::*;
13use crate::models::timeout::*;
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use std::collections::HashMap;
17
18/// Gets the namespace to use by default for workflow definitions
19pub const DEFAULT_NAMESPACE: &str = "default";
20// Provides the default namespace if not specified during deserialization
21fn default_namespace() -> String {
22    DEFAULT_NAMESPACE.to_string()
23}
24
25/// Gets the latest ServerlessWorkflow DSL version to use by default for workflow definitions
26pub const LATEST_DSL_VERSION: &str = "1.0.3";
27// Provides the latest ServerlessWorkflow DSL version
28fn default_dsl_version() -> String {
29    LATEST_DSL_VERSION.to_string()
30}
31
32/// Gets the default version for workflow definitions per spec 1.0.3
33pub const DEFAULT_WORKFLOW_VERSION: &str = "1.0.0";
34fn default_workflow_version() -> String {
35    DEFAULT_WORKFLOW_VERSION.to_string()
36}
37
38// Provides the default runtime expression language
39fn default_runtime_expression_language() -> String {
40    RuntimeExpressionLanguage::JQ.to_string()
41}
42
43string_constants! {
44    /// Enumerates all supported runtime expression languages
45    RuntimeExpressionLanguage {
46        JQ => "jq",
47        JAVASCRIPT => "js",
48    }
49}
50
51/// Represents the definition of a workflow
52#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
53pub struct WorkflowDefinition {
54    /// Gets/sets an object used to document the defined workflow
55    pub document: WorkflowDefinitionMetadata,
56
57    /// Gets/sets the workflow's input definition, if any
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub input: Option<InputDataModelDefinition>,
60
61    /// Gets/sets a collection that contains reusable components for the workflow definition
62    #[serde(rename = "use", skip_serializing_if = "Option::is_none")]
63    pub use_: Option<ComponentDefinitionCollection>,
64
65    /// Gets/sets the workflow's timeout, if any
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub timeout: Option<OneOfTimeoutDefinitionOrReference>,
68
69    /// Gets/sets the workflow's output definition, if any
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub output: Option<OutputDataModelDefinition>,
72
73    /// Gets/sets the workflow's context data definition, if any
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub context: Option<ContextDataModelDefinition>,
76
77    /// Gets/sets the definition of the workflow's schedule, if any
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub schedule: Option<WorkflowScheduleDefinition>,
80
81    /// Gets/sets the configuration of how the runtime expressions
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub evaluate: Option<RuntimeExpressionEvaluationConfiguration>,
84
85    /// Gets/sets a name/value mapping of the tasks to perform
86    #[serde(rename = "do")]
87    pub do_: Map<String, TaskDefinition>,
88
89    /// Gets/sets a key/value mapping, if any, of additional information associated with the workflow
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub metadata: Option<HashMap<String, Value>>,
92}
93impl WorkflowDefinition {
94    /// Initializes a new workflow definition with the given document metadata
95    pub fn new(document: WorkflowDefinitionMetadata) -> Self {
96        Self {
97            document,
98            ..Default::default()
99        }
100    }
101}
102
103/// Represents the metadata of a workflow, including its name, version, and description.
104#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
105pub struct WorkflowDefinitionMetadata {
106    /// Gets/sets the version of the DSL used to define the workflow
107    /// Defaults to "1.0.3" per spec
108    #[serde(default = "default_dsl_version")]
109    pub dsl: String,
110
111    /// Gets/sets the workflow's namespace
112    ///
113    /// Defaults to [`DEFAULT_NAMESPACE`] if not specified.
114    #[serde(default = "default_namespace")]
115    pub namespace: String,
116
117    /// Gets/sets the workflow's name
118    pub name: String,
119
120    /// Gets/sets the workflow's semantic version. Defaults to "1.0.0" per spec 1.0.3
121    #[serde(default = "default_workflow_version")]
122    pub version: String,
123
124    /// Gets/sets the workflow's title, if any
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub title: Option<String>,
127
128    /// Gets/sets the workflow's Markdown summary, if any
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub summary: Option<String>,
131
132    /// Gets/sets a key/value mapping of the workflow's tags, if any
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub tags: Option<HashMap<String, String>>,
135
136    /// Gets/sets a key/value mapping, if any, of additional information associated with the workflow document
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub metadata: Option<HashMap<String, Value>>,
139}
140impl WorkflowDefinitionMetadata {
141    // Initializes a new workflow definition metadata
142    pub fn new(
143        namespace: &str,
144        name: &str,
145        version: &str,
146        title: Option<String>,
147        summary: Option<String>,
148        tags: Option<HashMap<String, String>>,
149    ) -> Self {
150        Self {
151            dsl: default_dsl_version(),
152            namespace: namespace.to_owned(),
153            name: name.to_owned(),
154            version: version.to_owned(),
155            title,
156            summary,
157            tags,
158            metadata: None,
159        }
160    }
161}
162
163/// Represents the definition of a workflow's context data
164#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
165pub struct ContextDataModelDefinition {
166    /// Gets/sets the schema, if any, that defines and describes the context data
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub schema: Option<SchemaDefinition>,
169
170    /// Gets/sets a runtime expression, if any, used to set the content of the workflow context
171    #[serde(rename = "as", skip_serializing_if = "Option::is_none")]
172    pub as_: Option<serde_json::Value>,
173}
174
175/// Represents the definition of a workflow's schedule
176#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
177pub struct WorkflowScheduleDefinition {
178    /// Gets/sets an object used to document the defined workflow
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub every: Option<Duration>,
181
182    /// Gets/sets the schedule using a CRON expression, e.g., '0 0 * * *' for daily at midnight.
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub cron: Option<String>,
185
186    /// Gets/sets a delay duration, if any, that the workflow must wait before starting again after it completes. In other words, when this workflow completes, it should run again after the specified amount of time.
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub after: Option<Duration>,
189
190    /// Gets/sets the events that trigger the workflow execution
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub on: Option<EventConsumptionStrategyDefinition>,
193}
194
195string_constants! {
196    /// Enumerates all supported runtime expression evaluation modes
197    RuntimeExpressionEvaluationMode {
198        STRICT => "strict",
199        LOOSE => "loose",
200    }
201}
202
203string_constants! {
204    /// Enumerates all supported runtime expression argument names
205    RuntimeExpressions {
206        RUNTIME => "runtime",
207        WORKFLOW => "workflow",
208        CONTEXT => "context",
209        ITEM => "item",
210        INDEX => "index",
211        OUTPUT => "output",
212        SECRET => "secret",
213        TASK => "task",
214        INPUT => "input",
215        ERROR => "error",
216        AUTHORIZATION => "authorization",
217    }
218}
219
220/// Represents an object used to configure the workflow's runtime expression evaluation
221#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
222pub struct RuntimeExpressionEvaluationConfiguration {
223    /// Gets/sets the language used for writing runtime expressions
224    #[serde(default = "default_runtime_expression_language")]
225    pub language: String,
226
227    /// Gets/sets the evaluation mode used for runtime expressions. Defaults to 'strict'.
228    /// - `strict`: requires all expressions to be enclosed within `${ }` for proper identification and evaluation.
229    /// - `loose`: evaluates any value provided. If the evaluation fails, it results in a string with the expression as its content.
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub mode: Option<String>,
232}
233
234/// Represents a collection of workflow components
235#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
236pub struct ComponentDefinitionCollection {
237    /// Gets/sets a name/value mapping of the workflow's reusable authentication policies
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub authentications: Option<HashMap<String, ReferenceableAuthenticationPolicy>>,
240
241    /// Gets/sets a name/value mapping of the catalogs, if any, from which to import reusable components used within the workflow
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub catalogs: Option<HashMap<String, CatalogDefinition>>,
244
245    /// Gets/sets a name/value mapping of the workflow's errors, if any
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub errors: Option<HashMap<String, ErrorDefinition>>,
248
249    /// Gets/sets a list containing the workflow's extensions, if any
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub extensions: Option<Vec<HashMap<String, ExtensionDefinition>>>,
252
253    /// Gets/sets a name/value mapping of the workflow's reusable functions
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub functions: Option<HashMap<String, TaskDefinition>>,
256
257    /// Gets/sets a name/value mapping of the workflow's reusable retry policies
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub retries: Option<HashMap<String, RetryPolicyDefinition>>,
260
261    /// Gets/sets a list containing the workflow's secrets
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub secrets: Option<Vec<String>>,
264
265    /// Gets/sets a name/value mapping of the workflow's reusable timeouts
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub timeouts: Option<HashMap<String, TimeoutDefinition>>,
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    fn test_workflow_metadata_deserialize() {
276        let json = r#"{
277            "dsl": "1.0.0",
278            "namespace": "test-ns",
279            "name": "test-workflow",
280            "version": "1.0.0",
281            "title": "Test Workflow",
282            "summary": "A test workflow"
283        }"#;
284        let meta: WorkflowDefinitionMetadata = serde_json::from_str(json).unwrap();
285        assert_eq!(meta.dsl, "1.0.0");
286        assert_eq!(meta.namespace, "test-ns");
287        assert_eq!(meta.name, "test-workflow");
288        assert_eq!(meta.version, "1.0.0");
289        assert_eq!(meta.title, Some("Test Workflow".to_string()));
290    }
291
292    #[test]
293    fn test_workflow_metadata_defaults() {
294        let json = r#"{
295            "dsl": "1.0.0",
296            "name": "minimal",
297            "version": "0.1.0"
298        }"#;
299        let meta: WorkflowDefinitionMetadata = serde_json::from_str(json).unwrap();
300        assert_eq!(meta.dsl, "1.0.0");
301        assert_eq!(meta.namespace, "default");
302    }
303
304    #[test]
305    fn test_workflow_metadata_roundtrip() {
306        let json = r#"{
307            "dsl": "1.0.0",
308            "namespace": "test",
309            "name": "myflow",
310            "version": "1.0.0"
311        }"#;
312        let meta: WorkflowDefinitionMetadata = serde_json::from_str(json).unwrap();
313        let serialized = serde_json::to_string(&meta).unwrap();
314        let deserialized: WorkflowDefinitionMetadata = serde_json::from_str(&serialized).unwrap();
315        assert_eq!(meta, deserialized);
316    }
317
318    #[test]
319    fn test_schedule_cron() {
320        let json = r#"{"cron": "0 0 * * *"}"#;
321        let schedule: WorkflowScheduleDefinition = serde_json::from_str(json).unwrap();
322        assert_eq!(schedule.cron, Some("0 0 * * *".to_string()));
323        assert!(schedule.every.is_none());
324    }
325
326    #[test]
327    fn test_schedule_every() {
328        let json = r#"{"every": {"minutes": 30}}"#;
329        let schedule: WorkflowScheduleDefinition = serde_json::from_str(json).unwrap();
330        assert!(schedule.every.is_some());
331        assert!(schedule.cron.is_none());
332    }
333
334    #[test]
335    fn test_schedule_roundtrip() {
336        let json = r#"{"cron": "0 0 * * *"}"#;
337        let schedule: WorkflowScheduleDefinition = serde_json::from_str(json).unwrap();
338        let serialized = serde_json::to_string(&schedule).unwrap();
339        let deserialized: WorkflowScheduleDefinition = serde_json::from_str(&serialized).unwrap();
340        assert_eq!(schedule, deserialized);
341    }
342
343    #[test]
344    fn test_component_collection_deserialize() {
345        let json = r#"{
346            "secrets": ["dbPassword", "apiKey"],
347            "authentications": {
348                "basicAuth": {"basic": {"username": "admin", "password": "secret"}}
349            }
350        }"#;
351        let components: ComponentDefinitionCollection = serde_json::from_str(json).unwrap();
352        assert!(components.secrets.is_some());
353        assert!(components.authentications.is_some());
354    }
355
356    #[test]
357    fn test_component_collection_roundtrip() {
358        let json = r#"{
359            "secrets": ["dbPassword"]
360        }"#;
361        let components: ComponentDefinitionCollection = serde_json::from_str(json).unwrap();
362        let serialized = serde_json::to_string(&components).unwrap();
363        let deserialized: ComponentDefinitionCollection =
364            serde_json::from_str(&serialized).unwrap();
365        assert_eq!(components, deserialized);
366    }
367
368    #[test]
369    fn test_context_data_model_deserialize() {
370        let json = r#"{
371            "schema": {"format": "json", "document": {"type": "object"}},
372            "as": {"sessionId": "abc123"}
373        }"#;
374        let context: ContextDataModelDefinition = serde_json::from_str(json).unwrap();
375        assert!(context.schema.is_some());
376        assert!(context.as_.is_some());
377    }
378
379    #[test]
380    fn test_runtime_expression_config() {
381        let json = r#"{
382            "language": "jq",
383            "mode": "strict"
384        }"#;
385        let config: RuntimeExpressionEvaluationConfiguration = serde_json::from_str(json).unwrap();
386        assert_eq!(config.language, "jq");
387        assert_eq!(config.mode, Some("strict".to_string()));
388    }
389
390    #[test]
391    fn test_runtime_expression_config_defaults() {
392        let json = r#"{}"#;
393        let config: RuntimeExpressionEvaluationConfiguration = serde_json::from_str(json).unwrap();
394        assert_eq!(config.language, "jq");
395        assert!(config.mode.is_none());
396    }
397
398    // Additional tests matching Go SDK's workflow_test.go Use definition tests
399
400    #[test]
401    fn test_use_definition_comprehensive_deserialize() {
402        let json = r#"{
403            "secrets": ["secret1", "secret2"],
404            "timeouts": {"timeout1": {"after": "PT1M"}}
405        }"#;
406        let use_: ComponentDefinitionCollection = serde_json::from_str(json).unwrap();
407
408        // Secrets
409        assert!(use_.secrets.is_some());
410        let secrets = use_.secrets.as_ref().unwrap();
411        assert_eq!(secrets.len(), 2);
412        assert!(secrets.contains(&"secret1".to_string()));
413        assert!(secrets.contains(&"secret2".to_string()));
414
415        // Timeouts
416        assert!(use_.timeouts.is_some());
417        let timeouts = use_.timeouts.as_ref().unwrap();
418        assert!(timeouts.contains_key("timeout1"));
419    }
420
421    #[test]
422    fn test_use_definition_minimal() {
423        let json = r#"{
424            "secrets": ["mySecret"]
425        }"#;
426        let use_: ComponentDefinitionCollection = serde_json::from_str(json).unwrap();
427        assert!(use_.secrets.is_some());
428        assert!(use_.authentications.is_none());
429        assert!(use_.errors.is_none());
430        assert!(use_.extensions.is_none());
431        assert!(use_.retries.is_none());
432        assert!(use_.timeouts.is_none());
433        assert!(use_.catalogs.is_none());
434    }
435
436    #[test]
437    fn test_use_definition_empty() {
438        let json = r#"{}"#;
439        let use_: ComponentDefinitionCollection = serde_json::from_str(json).unwrap();
440        assert!(use_.authentications.is_none());
441        assert!(use_.secrets.is_none());
442    }
443
444    #[test]
445    fn test_use_definition_with_catalogs_roundtrip() {
446        let json = r#"{
447            "catalogs": {"default": {"endpoint": "http://example.com/catalog"}}
448        }"#;
449        let use_: ComponentDefinitionCollection = serde_json::from_str(json).unwrap();
450        let serialized = serde_json::to_string(&use_).unwrap();
451        let deserialized: ComponentDefinitionCollection =
452            serde_json::from_str(&serialized).unwrap();
453        assert_eq!(use_, deserialized);
454    }
455
456    #[test]
457    fn test_use_definition_with_timeouts_roundtrip() {
458        let json = r#"{
459            "timeouts": {"short": {"after": "PT10S"}, "long": {"after": "PT1H"}}
460        }"#;
461        let use_: ComponentDefinitionCollection = serde_json::from_str(json).unwrap();
462        let serialized = serde_json::to_string(&use_).unwrap();
463        let deserialized: ComponentDefinitionCollection =
464            serde_json::from_str(&serialized).unwrap();
465        assert_eq!(use_, deserialized);
466    }
467
468    #[test]
469    fn test_document_tags_and_metadata() {
470        // Matches Go SDK's Document_JSONMarshal/Unmarshal tests
471        let json = r#"{
472            "dsl": "1.0.0",
473            "namespace": "example-namespace",
474            "name": "example-name",
475            "version": "1.0.0",
476            "title": "Example Workflow",
477            "summary": "This is a sample workflow document.",
478            "tags": {"env": "prod", "team": "workflow"},
479            "metadata": {"author": "John Doe", "created": "2025-01-01"}
480        }"#;
481        let meta: WorkflowDefinitionMetadata = serde_json::from_str(json).unwrap();
482        assert_eq!(meta.dsl, "1.0.0");
483        assert_eq!(meta.namespace, "example-namespace");
484        assert_eq!(meta.name, "example-name");
485        assert_eq!(meta.version, "1.0.0");
486        assert_eq!(meta.title, Some("Example Workflow".to_string()));
487        assert_eq!(
488            meta.summary,
489            Some("This is a sample workflow document.".to_string())
490        );
491        assert!(meta.tags.is_some());
492        let tags = meta.tags.as_ref().unwrap();
493        assert_eq!(tags.get("env").map(|s| s.as_str()), Some("prod"));
494        assert_eq!(tags.get("team").map(|s| s.as_str()), Some("workflow"));
495        assert!(meta.metadata.is_some());
496        let md = meta.metadata.as_ref().unwrap();
497        assert!(md.contains_key("author"));
498    }
499
500    // Additional tests matching Go SDK's TestUse_UnmarshalJSON - individual sub-types
501
502    #[test]
503    fn test_use_authentications_deserialize() {
504        let json = r#"{"authentications": {"auth1": {"basic": {"username": "alice", "password": "secret"}}}}"#;
505        let use_: ComponentDefinitionCollection = serde_json::from_str(json).unwrap();
506        assert!(use_.authentications.is_some());
507        let auths = use_.authentications.as_ref().unwrap();
508        assert!(auths.contains_key("auth1"));
509    }
510
511    #[test]
512    fn test_use_errors_deserialize() {
513        let json = r#"{"errors": {"error1": {"type": "http://example.com/errors", "title": "Not Found", "status": 404}}}"#;
514        let use_: ComponentDefinitionCollection = serde_json::from_str(json).unwrap();
515        assert!(use_.errors.is_some());
516        let errors = use_.errors.as_ref().unwrap();
517        assert!(errors.contains_key("error1"));
518    }
519
520    #[test]
521    fn test_use_retries_deserialize() {
522        let json = r#"{"retries": {"retry1": {"delay": {"seconds": 5}, "limit": {"attempt": {"count": 3}}}}}"#;
523        let use_: ComponentDefinitionCollection = serde_json::from_str(json).unwrap();
524        assert!(use_.retries.is_some());
525        let retries = use_.retries.as_ref().unwrap();
526        assert!(retries.contains_key("retry1"));
527    }
528
529    #[test]
530    fn test_use_extensions_deserialize() {
531        let json = r#"{"extensions": [{"ext1": {"extend": "call"}}]}"#;
532        let use_: ComponentDefinitionCollection = serde_json::from_str(json).unwrap();
533        assert!(use_.extensions.is_some());
534    }
535
536    #[test]
537    fn test_use_functions_deserialize() {
538        let json = r#"{"functions": {"func1": {"set": {"result": "ok"}}}}"#;
539        let use_: ComponentDefinitionCollection = serde_json::from_str(json).unwrap();
540        assert!(use_.functions.is_some());
541        let funcs = use_.functions.as_ref().unwrap();
542        assert!(funcs.contains_key("func1"));
543    }
544
545    #[test]
546    fn test_use_authentications_roundtrip() {
547        let json = r#"{"authentications": {"auth1": {"basic": {"username": "alice", "password": "secret"}}}}"#;
548        let use_: ComponentDefinitionCollection = serde_json::from_str(json).unwrap();
549        let serialized = serde_json::to_string(&use_).unwrap();
550        let deserialized: ComponentDefinitionCollection =
551            serde_json::from_str(&serialized).unwrap();
552        assert_eq!(use_, deserialized);
553    }
554
555    #[test]
556    fn test_use_errors_roundtrip() {
557        let json = r#"{"errors": {"error1": {"type": "http://example.com/errors", "title": "Not Found", "status": 404}}}"#;
558        let use_: ComponentDefinitionCollection = serde_json::from_str(json).unwrap();
559        let serialized = serde_json::to_string(&use_).unwrap();
560        let deserialized: ComponentDefinitionCollection =
561            serde_json::from_str(&serialized).unwrap();
562        assert_eq!(use_, deserialized);
563    }
564
565    #[test]
566    fn test_use_retries_roundtrip() {
567        let json = r#"{"retries": {"retry1": {"delay": {"seconds": 5}, "limit": {"attempt": {"count": 3}}}}}"#;
568        let use_: ComponentDefinitionCollection = serde_json::from_str(json).unwrap();
569        let serialized = serde_json::to_string(&use_).unwrap();
570        let deserialized: ComponentDefinitionCollection =
571            serde_json::from_str(&serialized).unwrap();
572        assert_eq!(use_, deserialized);
573    }
574
575    #[test]
576    fn test_use_secrets_roundtrip() {
577        let json = r#"{"secrets": ["secret1", "secret2"]}"#;
578        let use_: ComponentDefinitionCollection = serde_json::from_str(json).unwrap();
579        let serialized = serde_json::to_string(&use_).unwrap();
580        let deserialized: ComponentDefinitionCollection =
581            serde_json::from_str(&serialized).unwrap();
582        assert_eq!(use_, deserialized);
583    }
584}