Skip to main content

connector_client/workflow/
value_expr.rs

1use super::{Condition, Locator, Step, StepOp, WorkflowError};
2use crate::outcome::ExecutionOutcome;
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use serde_json::{json, Value};
5use std::collections::BTreeMap;
6
7#[derive(Debug, Clone, PartialEq)]
8pub enum ValueExpr {
9    Literal(Value),
10    FromInput(InputRef),
11    FromStep(StepRef),
12}
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct InputRef {
16    pub key: String,
17}
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase", deny_unknown_fields)]
20pub struct StepRef {
21    pub step_id: String,
22    pub pointer: String,
23}
24impl Serialize for ValueExpr {
25    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
26        match self {
27            Self::Literal(v) => json!({"literal":v}),
28            Self::FromInput(v) => json!({"fromInput":v}),
29            Self::FromStep(v) => json!({"fromStep":v}),
30        }
31        .serialize(s)
32    }
33}
34impl<'de> Deserialize<'de> for ValueExpr {
35    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
36        let value = Value::deserialize(d)?;
37        match value {
38            Value::String(_) | Value::Bool(_) | Value::Number(_) => Ok(Self::Literal(value)),
39            Value::Object(object) if object.len() == 1 => {
40                let (key, value) = object.into_iter().next().expect("length checked");
41                match key.as_str() {
42                    "literal" => Ok(Self::Literal(value)),
43                    "fromInput" => serde_json::from_value(value)
44                        .map(Self::FromInput)
45                        .map_err(serde::de::Error::custom),
46                    "fromStep" => {
47                        let reference: StepRef =
48                            serde_json::from_value(value).map_err(serde::de::Error::custom)?;
49                        validate_pointer(&reference.pointer).map_err(serde::de::Error::custom)?;
50                        Ok(Self::FromStep(reference))
51                    }
52                    _ => Err(serde::de::Error::custom(
53                        "expression must have one of literal, fromInput, fromStep",
54                    )),
55                }
56            }
57            _ => Err(serde::de::Error::custom(
58                "expression must be a scalar or an object with exactly one expression key",
59            )),
60        }
61    }
62}
63impl ValueExpr {
64    pub fn literal(value: Value) -> Self {
65        Self::Literal(value)
66    }
67    pub fn as_literal(&self) -> Option<&Value> {
68        match self {
69            Self::Literal(value) => Some(value),
70            _ => None,
71        }
72    }
73    pub fn resolve(
74        &self,
75        inputs: &BTreeMap<String, Value>,
76        outcomes: &BTreeMap<String, ExecutionOutcome>,
77    ) -> Result<Value, WorkflowError> {
78        let value = match self {
79            Self::Literal(v) => v,
80            Self::FromInput(reference) => inputs.get(&reference.key).ok_or_else(|| {
81                WorkflowError::new(
82                    "binding_missing",
83                    "binding",
84                    "Required workflow input is missing",
85                )
86            })?,
87            Self::FromStep(reference) => {
88                validate_pointer(&reference.pointer)?;
89                let outcome = outcomes
90                    .get(&reference.step_id)
91                    .filter(|outcome| outcome.is_success(false))
92                    .ok_or_else(|| {
93                        WorkflowError::new(
94                            "binding_missing",
95                            "binding",
96                            "Referenced step has not succeeded",
97                        )
98                    })?;
99                outcome.data.pointer(&reference.pointer).ok_or_else(|| {
100                    WorkflowError::new(
101                        "binding_missing",
102                        "binding",
103                        "Referenced result path is missing",
104                    )
105                })?
106            }
107        };
108        if serde_json::to_vec(value).map_or(true, |bytes| bytes.len() > super::MAX_BINDING_BYTES) {
109            return Err(WorkflowError::new(
110                "invalid_spec",
111                "binding",
112                "Resolved binding exceeds the value size limit",
113            ));
114        }
115        Ok(value.clone())
116    }
117}
118
119/// Validate RFC 6901 escapes instead of silently accepting malformed pointers.
120pub fn validate_pointer(pointer: &str) -> Result<(), WorkflowError> {
121    if pointer.len() > 4096 || (!pointer.is_empty() && !pointer.starts_with('/')) {
122        return Err(WorkflowError::new(
123            "invalid_spec",
124            "validating",
125            "JSON Pointer must be empty or begin with / and fit within 4096 bytes",
126        ));
127    }
128    let mut chars = pointer.chars();
129    while let Some(ch) = chars.next() {
130        if ch == '~' && !matches!(chars.next(), Some('0' | '1')) {
131            return Err(WorkflowError::new(
132                "invalid_spec",
133                "validating",
134                "JSON Pointer has an invalid escape",
135            ));
136        }
137    }
138    Ok(())
139}
140fn resolve_expr(
141    expr: &mut ValueExpr,
142    inputs: &BTreeMap<String, Value>,
143    outcomes: &BTreeMap<String, ExecutionOutcome>,
144    string: bool,
145    nonempty: bool,
146) -> Result<(), WorkflowError> {
147    let value = expr.resolve(inputs, outcomes)?;
148    if string && !value.is_string() {
149        return Err(WorkflowError::new(
150            "binding_type_mismatch",
151            "binding",
152            "Expression requires a string value",
153        ));
154    }
155    if nonempty && value.as_str().is_some_and(|v| v.trim().is_empty()) {
156        return Err(WorkflowError::new(
157            "binding_type_mismatch",
158            "binding",
159            "Identity expression requires a non-empty string",
160        ));
161    }
162    *expr = ValueExpr::Literal(value);
163    Ok(())
164}
165fn resolve_locator(
166    target: &mut Locator,
167    inputs: &BTreeMap<String, Value>,
168    outcomes: &BTreeMap<String, ExecutionOutcome>,
169) -> Result<(), WorkflowError> {
170    resolve_expr(&mut target.value, inputs, outcomes, true, true)?;
171    if let Some(name) = &mut target.name {
172        resolve_expr(name, inputs, outcomes, true, false)?;
173    }
174    if let Some(scope) = &mut target.scope {
175        resolve_locator(scope, inputs, outcomes)?;
176    }
177    if let Some(entity) = &mut target.entity {
178        resolve_expr(&mut entity.value, inputs, outcomes, true, true)?;
179    }
180    Ok(())
181}
182pub fn resolve_condition(
183    condition: &Condition,
184    inputs: &BTreeMap<String, Value>,
185    outcomes: &BTreeMap<String, ExecutionOutcome>,
186) -> Result<Condition, WorkflowError> {
187    let mut condition = condition.clone();
188    match &mut condition {
189        Condition::Element { target, .. } => resolve_locator(target, inputs, outcomes)?,
190        Condition::ValueEquals { target, expected }
191        | Condition::TextContains { target, expected }
192        | Condition::AttributeEquals {
193            target, expected, ..
194        } => {
195            resolve_locator(target, inputs, outcomes)?;
196            resolve_expr(expected, inputs, outcomes, true, false)?;
197        }
198        Condition::Result { expected, .. } => {
199            if let Some(expected) = expected {
200                resolve_expr(expected, inputs, outcomes, false, false)?;
201            }
202        }
203        Condition::All { conditions } | Condition::Any { conditions } => {
204            for item in conditions {
205                *item = resolve_condition(item, inputs, outcomes)?;
206            }
207        }
208    }
209    Ok(condition)
210}
211pub fn resolve_step(
212    step: &Step,
213    inputs: &BTreeMap<String, Value>,
214    outcomes: &BTreeMap<String, ExecutionOutcome>,
215) -> Result<Step, WorkflowError> {
216    let mut step = step.clone();
217    match &mut step.op {
218        StepOp::Click { target } | StepOp::Query { target, .. } => {
219            resolve_locator(target, inputs, outcomes)?
220        }
221        StepOp::Fill { target, value } | StepOp::Type { target, value } => {
222            resolve_locator(target, inputs, outcomes)?;
223            resolve_expr(value, inputs, outcomes, true, false)?;
224        }
225        StepOp::Press { target, key } => {
226            if let Some(target) = target {
227                resolve_locator(target, inputs, outcomes)?;
228            }
229            resolve_expr(key, inputs, outcomes, true, true)?;
230        }
231        StepOp::Wait { condition } => *condition = resolve_condition(condition, inputs, outcomes)?,
232        StepOp::Tool { args, bindings, .. } => {
233            for (pointer, expr) in bindings.iter() {
234                validate_pointer(pointer)?;
235                if pointer.is_empty() {
236                    return Err(WorkflowError::new(
237                        "invalid_spec",
238                        "binding",
239                        "Tool binding cannot replace the entire arguments object",
240                    ));
241                }
242                let value = expr.resolve(inputs, outcomes)?;
243                let slot = args.pointer_mut(pointer).ok_or_else(|| {
244                    WorkflowError::new(
245                        "binding_missing",
246                        "binding",
247                        "Tool argument binding path does not exist",
248                    )
249                })?;
250                *slot = value;
251            }
252            bindings.clear();
253        }
254    }
255    if let Some(expect) = &step.expect {
256        step.expect = Some(resolve_condition(expect, inputs, outcomes)?);
257    }
258    if serde_json::to_vec(&step).map_or(true, |bytes| bytes.len() > super::MAX_SPEC_BYTES) {
259        return Err(WorkflowError::new(
260            "invalid_spec",
261            "binding",
262            "Resolved step exceeds the 256 KiB size limit",
263        ));
264    }
265    Ok(step)
266}