Skip to main content

swf_core/validation/
task.rs

1use super::document::validate_timeout;
2use super::enum_validators::*;
3use super::one_of_validators::{validate_backoff_one_of, validate_process_type_one_of};
4use super::{
5    is_valid_hostname, validate_required_hostname, validate_required_semver, ValidationResult,
6    ValidationRule,
7};
8use crate::models::map::Map;
9use crate::models::retry::OneOfRetryPolicyDefinitionOrReference;
10use crate::models::task::*;
11
12/// Validates a map of task definitions
13pub fn validate_task_map(
14    tasks: &Map<String, TaskDefinition>,
15    prefix: &str,
16    result: &mut ValidationResult,
17) {
18    for (name, task) in &tasks.entries {
19        validate_task(task, &format!("{}.{}", prefix, name), result);
20    }
21}
22
23/// Validates a single task definition
24pub(crate) fn validate_task(task: &TaskDefinition, prefix: &str, result: &mut ValidationResult) {
25    match task {
26        TaskDefinition::Call(call) => validate_call_task(call, prefix, result),
27        TaskDefinition::Do(do_task) => {
28            validate_common_fields(&do_task.common, prefix, result);
29            validate_task_map(&do_task.do_, &format!("{}.do", prefix), result);
30        }
31        TaskDefinition::Emit(emit) => {
32            validate_common_fields(&emit.common, prefix, result);
33        }
34        TaskDefinition::For(for_task) => {
35            validate_common_fields(&for_task.common, prefix, result);
36            if for_task.for_.each.is_empty() {
37                result.add_error(
38                    &format!("{}.for.each", prefix),
39                    ValidationRule::Required,
40                    "'each' is required in for loop",
41                );
42            }
43            if for_task.for_.in_.is_empty() {
44                result.add_error(
45                    &format!("{}.for.in", prefix),
46                    ValidationRule::Required,
47                    "'in' is required in for loop",
48                );
49            }
50            validate_task_map(&for_task.do_, &format!("{}.do", prefix), result);
51        }
52        TaskDefinition::Fork(fork) => {
53            validate_common_fields(&fork.common, prefix, result);
54            validate_task_map(
55                &fork.fork.branches,
56                &format!("{}.fork.branches", prefix),
57                result,
58            );
59        }
60        TaskDefinition::Listen(listen) => {
61            validate_common_fields(&listen.common, prefix, result);
62        }
63        TaskDefinition::Raise(raise) => {
64            validate_common_fields(&raise.common, prefix, result);
65        }
66        TaskDefinition::Run(run) => {
67            validate_common_fields(&run.common, prefix, result);
68            validate_run_task(&run.run, prefix, result);
69        }
70        TaskDefinition::Set(set) => {
71            validate_common_fields(&set.common, prefix, result);
72            validate_set_task(&set.set, prefix, result);
73        }
74        TaskDefinition::Switch(switch) => {
75            validate_common_fields(&switch.common, prefix, result);
76            validate_switch_task(switch, prefix, result);
77        }
78        TaskDefinition::Try(try_task) => {
79            validate_common_fields(&try_task.common, prefix, result);
80            validate_task_map(&try_task.try_, &format!("{}.try", prefix), result);
81            // Validate retry backoff oneOf if retry is set
82            if let Some(OneOfRetryPolicyDefinitionOrReference::Retry(ref policy)) =
83                try_task.catch.retry
84            {
85                if let Some(ref backoff) = policy.backoff {
86                    validate_backoff_one_of(
87                        backoff,
88                        &format!("{}.catch.retry.backoff", prefix),
89                        result,
90                    );
91                }
92            }
93        }
94        TaskDefinition::Wait(wait) => {
95            validate_common_fields(&wait.common, prefix, result);
96        }
97        TaskDefinition::Custom(custom) => {
98            validate_common_fields(&custom.common, prefix, result);
99        }
100    }
101}
102
103/// Validates a run task configuration
104pub(crate) fn validate_run_task(
105    run: &ProcessTypeDefinition,
106    prefix: &str,
107    result: &mut ValidationResult,
108) {
109    // Enforce oneOf: exactly one of container/script/shell/workflow must be set
110    validate_process_type_one_of(run, prefix, result);
111
112    if let Some(ref container) = run.container {
113        if let Some(ref pull_policy) = container.pull_policy {
114            validate_pull_policy(pull_policy, prefix, result);
115        }
116        if let Some(ref lifetime) = container.lifetime {
117            validate_container_lifetime(lifetime, prefix, result);
118        }
119    }
120    if let Some(ref script) = run.script {
121        validate_script_language(&script.language, prefix, result);
122    }
123    if let Some(ref workflow) = run.workflow {
124        validate_workflow_process(workflow, prefix, result);
125    }
126}
127
128/// Validates a call task definition
129pub(crate) fn validate_call_task(
130    call: &CallTaskDefinition,
131    prefix: &str,
132    result: &mut ValidationResult,
133) {
134    validate_common_fields(call.common_fields(), prefix, result);
135    match call {
136        CallTaskDefinition::HTTP(http) => {
137            if http.with.method.is_empty() {
138                result.add_error(
139                    &format!("{}.with.method", prefix),
140                    ValidationRule::Required,
141                    "HTTP method is required",
142                );
143            } else {
144                validate_http_method(&http.with.method, prefix, result);
145            }
146            if let Some(ref output) = http.with.output {
147                validate_http_output(output, &format!("{}.with", prefix), result);
148            }
149        }
150        CallTaskDefinition::GRPC(grpc) => {
151            if grpc.with.method.is_empty() {
152                result.add_error(
153                    &format!("{}.with.method", prefix),
154                    ValidationRule::Required,
155                    "GRPC method is required",
156                );
157            }
158            if grpc.with.service.name.is_empty() {
159                result.add_error(
160                    &format!("{}.with.service.name", prefix),
161                    ValidationRule::Required,
162                    "GRPC service name is required",
163                );
164            }
165            // Go SDK: validate:"required,hostname_rfc1123"
166            if grpc.with.service.host.is_empty() {
167                result.add_error(
168                    &format!("{}.with.service.host", prefix),
169                    ValidationRule::Required,
170                    "GRPC service host is required",
171                );
172            } else if !is_valid_hostname(&grpc.with.service.host) {
173                result.add_error(
174                    &format!("{}.with.service.host", prefix),
175                    ValidationRule::Hostname,
176                    "GRPC service host must be a valid RFC 1123 hostname",
177                );
178            }
179        }
180        CallTaskDefinition::OpenAPI(openapi) => {
181            if openapi.with.operation_id.is_empty() {
182                result.add_error(
183                    &format!("{}.with.operationId", prefix),
184                    ValidationRule::Required,
185                    "OpenAPI operationId is required",
186                );
187            }
188            if let Some(ref output) = openapi.with.output {
189                validate_http_output(output, &format!("{}.with", prefix), result);
190            }
191        }
192        CallTaskDefinition::AsyncAPI(asyncapi) => {
193            if let Some(ref protocol) = asyncapi.with.protocol {
194                validate_asyncapi_protocol(protocol, &format!("{}.with", prefix), result);
195            }
196        }
197        CallTaskDefinition::A2A(a2a) => {
198            if a2a.with.method.is_empty() {
199                result.add_error(
200                    &format!("{}.with.method", prefix),
201                    ValidationRule::Required,
202                    "A2A method is required",
203                );
204            }
205        }
206        CallTaskDefinition::Function(func) => {
207            if func.call.is_empty() {
208                result.add_error(
209                    &format!("{}.call", prefix),
210                    ValidationRule::Required,
211                    "function name is required",
212                );
213            }
214        }
215    }
216}
217
218/// Validates common task definition fields
219pub(crate) fn validate_common_fields(
220    fields: &TaskDefinitionFields,
221    prefix: &str,
222    result: &mut ValidationResult,
223) {
224    if let Some(ref timeout) = fields.timeout {
225        validate_timeout(timeout, &format!("{}.timeout", prefix), result);
226    }
227}
228
229/// Validates a set task's value
230/// Matches Go SDK's validate:"required,min=1,dive"
231/// Set must have at least 1 key-value pair
232pub fn validate_set_task(set: &SetValue, prefix: &str, result: &mut ValidationResult) {
233    match set {
234        SetValue::Map(map) => {
235            if map.is_empty() {
236                result.add_error(
237                    &format!("{}.set", prefix),
238                    ValidationRule::Required,
239                    "set task must have at least one key-value pair",
240                );
241            }
242        }
243        SetValue::Expression(expr) => {
244            if expr.is_empty() {
245                result.add_error(
246                    &format!("{}.set", prefix),
247                    ValidationRule::Required,
248                    "set task expression must not be empty",
249                );
250            }
251        }
252    }
253}
254
255/// Validates a WorkflowProcessDefinition (sub-workflow reference)
256/// Matches Go SDK's validation tags:
257/// - namespace: validate:"required,hostname_rfc1123"
258/// - name: validate:"required,hostname_rfc1123"
259/// - version: validate:"required,semver_pattern"
260pub fn validate_workflow_process(
261    workflow: &WorkflowProcessDefinition,
262    prefix: &str,
263    result: &mut ValidationResult,
264) {
265    let p = &format!("{}.run.workflow", prefix);
266    validate_required_hostname(&workflow.namespace, &format!("{}.namespace", p), result);
267    validate_required_hostname(&workflow.name, &format!("{}.name", p), result);
268    validate_required_semver(&workflow.version, &format!("{}.version", p), result);
269}
270
271/// Validates a switch task definition
272/// Matches Go SDK's SwitchTask validation:
273/// - switch must have at least 1 case item (`validate:"required,min=1,dive,switch_item"`)
274/// - each switch item must have exactly 1 key (`validate_switch_item` where `len(switchItem) == 1`)
275/// - each case's `then` field is required (`validate:"required"`)
276pub fn validate_switch_task(
277    switch: &SwitchTaskDefinition,
278    prefix: &str,
279    result: &mut ValidationResult,
280) {
281    // Validate at least 1 switch case
282    if switch.switch.is_empty() {
283        result.add_error(
284            &format!("{}.switch", prefix),
285            ValidationRule::Required,
286            "switch task must have at least one case",
287        );
288    }
289
290    // Validate each switch item has exactly 1 key and then is required
291    for (i, (name, case_def)) in switch.switch.entries.iter().enumerate() {
292        let case_prefix = format!("{}.switch[{}]", prefix, i);
293        // Validate each case's then field
294        if case_def.then.is_none() {
295            result.add_error(
296                &format!("{}.{}.then", case_prefix, name),
297                ValidationRule::Required,
298                "switch case 'then' is required",
299            );
300        }
301    }
302}