1use crate::form::{FormNodeId, FormNodeType, FormTree};
13
14use formcalc_interpreter::interpreter::Interpreter;
15use formcalc_interpreter::lexer::tokenize;
16use formcalc_interpreter::parser;
17use formcalc_interpreter::value::Value;
18
19#[derive(Debug, thiserror::Error)]
21pub enum ScriptError {
22 #[error("FormCalc error in node '{node}': {message}")]
23 Execution { node: String, message: String },
24 #[error("Validation failed for node '{node}': {message}")]
25 ValidationFailed { node: String, message: String },
26}
27
28#[derive(Debug, Default)]
30pub struct ScriptResult {
31 pub updated_fields: Vec<FormNodeId>,
33 pub validation_errors: Vec<(FormNodeId, String)>,
35}
36
37pub fn run_calculations(form: &mut FormTree) -> Result<ScriptResult, ScriptError> {
43 let mut result = ScriptResult::default();
44 let mut interpreter = Interpreter::new();
45
46 let calc_nodes: Vec<(FormNodeId, String, String)> = form
48 .nodes
49 .iter()
50 .enumerate()
51 .filter_map(|(i, node)| {
52 node.calculate
53 .as_ref()
54 .map(|script| (FormNodeId(i), node.name.clone(), script.clone()))
55 })
56 .collect();
57
58 for (id, _name, script) in calc_nodes {
59 let value = match eval_script(&mut interpreter, &script) {
62 Ok(v) => v,
63 Err(_) => continue,
64 };
65
66 let value_str = value_to_string(&value);
68
69 let node = form.get_mut(id);
70 if let FormNodeType::Field { ref mut value } = node.node_type {
71 if *value != value_str {
72 *value = value_str;
73 result.updated_fields.push(id);
74 }
75 }
76 }
77
78 Ok(result)
79}
80
81pub fn run_validations(form: &FormTree) -> Result<ScriptResult, ScriptError> {
86 let mut result = ScriptResult::default();
87 let mut interpreter = Interpreter::new();
88
89 for (i, node) in form.nodes.iter().enumerate() {
90 if let Some(ref script) = node.validate {
91 let val =
92 eval_script(&mut interpreter, script).map_err(|e| ScriptError::Execution {
93 node: node.name.clone(),
94 message: e,
95 })?;
96
97 if !is_truthy(&val) {
98 let msg = format!(
99 "Validation script returned falsy value: {}",
100 value_to_string(&val)
101 );
102 result.validation_errors.push((FormNodeId(i), msg));
103 }
104 }
105 }
106
107 Ok(result)
108}
109
110pub fn prepare_form(form: &mut FormTree) -> Result<ScriptResult, ScriptError> {
115 let mut calc_result = run_calculations(form)?;
116 let val_result = run_validations(form)?;
117 calc_result.validation_errors = val_result.validation_errors;
118 Ok(calc_result)
119}
120
121fn eval_script(interpreter: &mut Interpreter, script: &str) -> Result<Value, String> {
123 let tokens = tokenize(script).map_err(|e| format!("Tokenize error: {e}"))?;
124 let ast = parser::parse(tokens).map_err(|e| format!("Parse error: {e}"))?;
125 interpreter
126 .exec(&ast)
127 .map_err(|e| format!("Runtime error: {e}"))
128}
129
130fn value_to_string(val: &Value) -> String {
132 match val {
133 Value::Number(n) => {
134 if *n == n.floor() && n.is_finite() {
136 format!("{}", *n as i64)
137 } else {
138 format!("{n}")
139 }
140 }
141 Value::String(s) => s.clone(),
142 Value::Null => String::new(),
143 }
144}
145
146fn is_truthy(val: &Value) -> bool {
148 match val {
149 Value::Number(n) => *n != 0.0,
150 Value::String(s) => !s.is_empty(),
151 Value::Null => false,
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::form::{FormNode, Occur};
159 use crate::text::FontMetrics;
160 use crate::types::{BoxModel, LayoutStrategy};
161
162 fn make_field_with_calc(
163 tree: &mut FormTree,
164 name: &str,
165 initial_value: &str,
166 calculate: Option<&str>,
167 ) -> FormNodeId {
168 tree.add_node(FormNode {
169 name: name.to_string(),
170 node_type: FormNodeType::Field {
171 value: initial_value.to_string(),
172 },
173 box_model: BoxModel {
174 width: Some(100.0),
175 height: Some(20.0),
176 max_width: f64::MAX,
177 max_height: f64::MAX,
178 ..Default::default()
179 },
180 layout: LayoutStrategy::Positioned,
181 children: vec![],
182 occur: Occur::once(),
183 font: FontMetrics::default(),
184 calculate: calculate.map(|s| s.to_string()),
185 validate: None,
186 column_widths: vec![],
187 col_span: 1,
188 })
189 }
190
191 #[test]
192 fn calculate_script_updates_field_value() {
193 let mut tree = FormTree::new();
194 make_field_with_calc(&mut tree, "Total", "0", Some("10 + 20"));
195
196 let result = run_calculations(&mut tree).unwrap();
197
198 assert_eq!(result.updated_fields.len(), 1);
199 if let FormNodeType::Field { value } = &tree.get(result.updated_fields[0]).node_type {
200 assert_eq!(value, "30");
201 } else {
202 panic!("Expected Field node");
203 }
204 }
205
206 #[test]
207 fn calculate_script_string_result() {
208 let mut tree = FormTree::new();
209 make_field_with_calc(
210 &mut tree,
211 "Greeting",
212 "",
213 Some("Concat(\"Hello\", \" \", \"World\")"),
214 );
215
216 let result = run_calculations(&mut tree).unwrap();
217
218 assert_eq!(result.updated_fields.len(), 1);
219 if let FormNodeType::Field { value } = &tree.get(result.updated_fields[0]).node_type {
220 assert_eq!(value, "Hello World");
221 }
222 }
223
224 #[test]
225 fn no_update_when_value_unchanged() {
226 let mut tree = FormTree::new();
227 make_field_with_calc(&mut tree, "Same", "42", Some("42"));
228
229 let result = run_calculations(&mut tree).unwrap();
230
231 assert_eq!(result.updated_fields.len(), 0); }
233
234 #[test]
235 fn fields_without_scripts_are_untouched() {
236 let mut tree = FormTree::new();
237 make_field_with_calc(&mut tree, "Static", "original", None);
238
239 let result = run_calculations(&mut tree).unwrap();
240
241 assert_eq!(result.updated_fields.len(), 0);
242 if let FormNodeType::Field { value } = &tree.get(FormNodeId(0)).node_type {
243 assert_eq!(value, "original");
244 }
245 }
246
247 #[test]
248 fn validation_passes_for_truthy() {
249 let mut tree = FormTree::new();
250 let id = tree.add_node(FormNode {
251 name: "Amount".to_string(),
252 node_type: FormNodeType::Field {
253 value: "100".to_string(),
254 },
255 box_model: BoxModel {
256 width: Some(100.0),
257 height: Some(20.0),
258 max_width: f64::MAX,
259 max_height: f64::MAX,
260 ..Default::default()
261 },
262 layout: LayoutStrategy::Positioned,
263 children: vec![],
264 occur: Occur::once(),
265 font: FontMetrics::default(),
266 calculate: None,
267 validate: Some("1".to_string()), column_widths: vec![],
269 col_span: 1,
270 });
271 let _ = id;
272
273 let result = run_validations(&tree).unwrap();
274 assert!(result.validation_errors.is_empty());
275 }
276
277 #[test]
278 fn validation_fails_for_falsy() {
279 let mut tree = FormTree::new();
280 tree.add_node(FormNode {
281 name: "Required".to_string(),
282 node_type: FormNodeType::Field {
283 value: "".to_string(),
284 },
285 box_model: BoxModel {
286 width: Some(100.0),
287 height: Some(20.0),
288 max_width: f64::MAX,
289 max_height: f64::MAX,
290 ..Default::default()
291 },
292 layout: LayoutStrategy::Positioned,
293 children: vec![],
294 occur: Occur::once(),
295 font: FontMetrics::default(),
296 calculate: None,
297 validate: Some("0".to_string()), column_widths: vec![],
299 col_span: 1,
300 });
301
302 let result = run_validations(&tree).unwrap();
303 assert_eq!(result.validation_errors.len(), 1);
304 }
305
306 #[test]
307 fn prepare_form_runs_both() {
308 let mut tree = FormTree::new();
309 make_field_with_calc(&mut tree, "Sum", "0", Some("5 * 3"));
311 tree.add_node(FormNode {
313 name: "Check".to_string(),
314 node_type: FormNodeType::Field {
315 value: "ok".to_string(),
316 },
317 box_model: BoxModel {
318 width: Some(100.0),
319 height: Some(20.0),
320 max_width: f64::MAX,
321 max_height: f64::MAX,
322 ..Default::default()
323 },
324 layout: LayoutStrategy::Positioned,
325 children: vec![],
326 occur: Occur::once(),
327 font: FontMetrics::default(),
328 calculate: None,
329 validate: Some("0".to_string()), column_widths: vec![],
331 col_span: 1,
332 });
333
334 let result = prepare_form(&mut tree).unwrap();
335
336 assert_eq!(result.updated_fields.len(), 1);
338 if let FormNodeType::Field { value } = &tree.get(FormNodeId(0)).node_type {
339 assert_eq!(value, "15");
340 }
341 assert_eq!(result.validation_errors.len(), 1);
343 }
344
345 #[test]
346 fn complex_calculation() {
347 let mut tree = FormTree::new();
348 make_field_with_calc(&mut tree, "Tax", "0", Some("Round(100 * 0.21, 2)"));
349
350 let result = run_calculations(&mut tree).unwrap();
351 assert_eq!(result.updated_fields.len(), 1);
352 if let FormNodeType::Field { value } = &tree.get(result.updated_fields[0]).node_type {
353 assert_eq!(value, "21");
354 }
355 }
356}