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