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
119pub 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}