ironflow_engine/context/steps/conditions.rs
1//! Branch conditions for [`WorkflowContext`].
2//!
3//! A handler branches with plain Rust `if`/`else`, which the planner cannot
4//! see. These two methods make a branch visible to
5//! [`Engine::plan_handler`](crate::engine::Engine::plan_handler) without
6//! changing what the handler does at run time.
7
8use serde::de::DeserializeOwned;
9
10use crate::context::WorkflowContext;
11use crate::error::EngineError;
12use crate::plan::{ConditionResult, lock_plan};
13
14/// Why a condition computed from a step output cannot be resolved by the
15/// planner.
16const DYNAMIC_REASON: &str =
17 "depends on a previous step's output, which is synthetic while planning";
18
19impl WorkflowContext {
20 /// Evaluate a named branch condition against the typed run input.
21 ///
22 /// The run payload is deserialized into `T`, exactly like
23 /// [`input`](Self::input), and `predicate` decides the branch on it. `label`
24 /// is a human-readable name for the branch, shown in the plan; it is never
25 /// parsed nor evaluated. In plan mode the result is also recorded as
26 /// [`ConditionResult::Evaluated`] on the next planned step, so the operator
27 /// sees which branch the plan followed and why.
28 ///
29 /// # Errors
30 ///
31 /// Returns [`EngineError::Store`] when the run payload cannot be read, and
32 /// [`EngineError::Serialization`] when it does not match `T`: a misspelled
33 /// field or variant fails the branch instead of silently taking the other
34 /// one.
35 ///
36 /// # Examples
37 ///
38 /// ```no_run
39 /// use ironflow_engine::context::WorkflowContext;
40 /// use ironflow_engine::config::ShellConfig;
41 /// use ironflow_engine::error::EngineError;
42 /// use serde::Deserialize;
43 ///
44 /// #[derive(Deserialize, PartialEq)]
45 /// #[serde(rename_all = "lowercase")]
46 /// enum Env {
47 /// Prod,
48 /// Staging,
49 /// }
50 ///
51 /// #[derive(Deserialize)]
52 /// struct DeployInput {
53 /// env: Env,
54 /// }
55 ///
56 /// # async fn example(ctx: &mut WorkflowContext) -> Result<(), EngineError> {
57 /// if ctx.when("production run", |i: &DeployInput| i.env == Env::Prod).await? {
58 /// ctx.shell("deploy-prod", ShellConfig::new("./deploy prod")).await?;
59 /// } else {
60 /// ctx.skip("deploy-prod", "not a production run").await?;
61 /// }
62 /// # Ok(())
63 /// # }
64 /// ```
65 pub async fn when<T, F>(&mut self, label: &str, predicate: F) -> Result<bool, EngineError>
66 where
67 T: DeserializeOwned,
68 F: FnOnce(&T) -> bool,
69 {
70 let input: T = self.input().await?;
71 let value = predicate(&input);
72
73 if let Some(plan) = self.plan().cloned() {
74 lock_plan(&plan).set_condition(ConditionResult::Evaluated {
75 expression: label.to_string(),
76 value,
77 });
78 }
79
80 Ok(value)
81 }
82
83 /// Record a branch condition whose value depends on a previous step's
84 /// output.
85 ///
86 /// Returns `value` unchanged; `label` names the branch in the plan. Under
87 /// planning, step outputs are synthetic,
88 /// so the condition is recorded as [`ConditionResult::Unevaluable`]: the
89 /// plan still follows the branch the synthetic output produces, and the
90 /// operator is told the other branch may run instead.
91 ///
92 /// # Examples
93 ///
94 /// ```no_run
95 /// use ironflow_engine::context::WorkflowContext;
96 /// use ironflow_engine::config::ShellConfig;
97 /// use ironflow_engine::error::EngineError;
98 ///
99 /// # async fn example(ctx: &mut WorkflowContext) -> Result<(), EngineError> {
100 /// let build = ctx.shell("build", ShellConfig::new("cargo build")).await?;
101 /// if ctx.when_dynamic("build succeeded", build.is_success()) {
102 /// ctx.shell("deploy", ShellConfig::new("./deploy")).await?;
103 /// }
104 /// # Ok(())
105 /// # }
106 /// ```
107 pub fn when_dynamic(&mut self, label: &str, value: bool) -> bool {
108 if let Some(plan) = self.plan().cloned() {
109 lock_plan(&plan).set_condition(ConditionResult::Unevaluable {
110 expression: label.to_string(),
111 reason: DYNAMIC_REASON.to_string(),
112 });
113 }
114
115 value
116 }
117}