use crate::form::{FormNodeId, FormNodeType, FormTree};
use formcalc_interpreter::interpreter::Interpreter;
use formcalc_interpreter::lexer::tokenize;
use formcalc_interpreter::parser;
use formcalc_interpreter::value::Value;
#[derive(Debug, thiserror::Error)]
pub enum ScriptError {
#[error("FormCalc error in node '{node}': {message}")]
Execution {
node: String,
message: String,
},
#[error("Validation failed for node '{node}': {message}")]
ValidationFailed {
node: String,
message: String,
},
}
#[derive(Debug, Default)]
pub struct ScriptResult {
pub updated_fields: Vec<FormNodeId>,
pub validation_errors: Vec<(FormNodeId, String)>,
}
pub fn run_calculations(form: &mut FormTree) -> Result<ScriptResult, ScriptError> {
let mut result = ScriptResult::default();
let mut interpreter = Interpreter::new();
let calc_nodes: Vec<(FormNodeId, String, String)> = form
.nodes
.iter()
.enumerate()
.filter_map(|(i, node)| {
node.calculate
.as_ref()
.map(|script| (FormNodeId(i), node.name.clone(), script.clone()))
})
.collect();
for (id, _name, script) in calc_nodes {
let value = match eval_script(&mut interpreter, &script) {
Ok(v) => v,
Err(_) => continue,
};
let value_str = value_to_string(&value);
let node = form.get_mut(id);
if let FormNodeType::Field { ref mut value } = node.node_type {
if *value != value_str {
*value = value_str;
result.updated_fields.push(id);
}
}
}
Ok(result)
}
pub fn run_validations(form: &FormTree) -> Result<ScriptResult, ScriptError> {
let mut result = ScriptResult::default();
let mut interpreter = Interpreter::new();
for (i, node) in form.nodes.iter().enumerate() {
if let Some(ref script) = node.validate {
let val =
eval_script(&mut interpreter, script).map_err(|e| ScriptError::Execution {
node: node.name.clone(),
message: e,
})?;
if !is_truthy(&val) {
let msg = format!(
"Validation script returned falsy value: {}",
value_to_string(&val)
);
result.validation_errors.push((FormNodeId(i), msg));
}
}
}
Ok(result)
}
pub fn prepare_form(form: &mut FormTree) -> Result<ScriptResult, ScriptError> {
let mut calc_result = run_calculations(form)?;
let val_result = run_validations(form)?;
calc_result.validation_errors = val_result.validation_errors;
Ok(calc_result)
}
fn eval_script(interpreter: &mut Interpreter, script: &str) -> Result<Value, String> {
let tokens = tokenize(script).map_err(|e| format!("Tokenize error: {e}"))?;
let ast = parser::parse(tokens).map_err(|e| format!("Parse error: {e}"))?;
interpreter
.exec(&ast)
.map_err(|e| format!("Runtime error: {e}"))
}
fn value_to_string(val: &Value) -> String {
match val {
Value::Number(n) => {
if *n == n.floor() && n.is_finite() {
format!("{}", *n as i64)
} else {
format!("{n}")
}
}
Value::String(s) => s.clone(),
Value::Null => String::new(),
}
}
fn is_truthy(val: &Value) -> bool {
match val {
Value::Number(n) => *n != 0.0,
Value::String(s) => !s.is_empty(),
Value::Null => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::form::{FormNode, Occur};
use crate::text::FontMetrics;
use crate::types::{BoxModel, LayoutStrategy};
fn make_field_with_calc(
tree: &mut FormTree,
name: &str,
initial_value: &str,
calculate: Option<&str>,
) -> FormNodeId {
tree.add_node(FormNode {
name: name.to_string(),
node_type: FormNodeType::Field {
value: initial_value.to_string(),
},
box_model: BoxModel {
width: Some(100.0),
height: Some(20.0),
max_width: f64::MAX,
max_height: f64::MAX,
..Default::default()
},
layout: LayoutStrategy::Positioned,
children: vec![],
occur: Occur::once(),
font: FontMetrics::default(),
calculate: calculate.map(|s| s.to_string()),
validate: None,
column_widths: vec![],
col_span: 1,
})
}
#[test]
fn calculate_script_updates_field_value() {
let mut tree = FormTree::new();
make_field_with_calc(&mut tree, "Total", "0", Some("10 + 20"));
let result = run_calculations(&mut tree).unwrap();
assert_eq!(result.updated_fields.len(), 1);
if let FormNodeType::Field { value } = &tree.get(result.updated_fields[0]).node_type {
assert_eq!(value, "30");
} else {
panic!("Expected Field node");
}
}
#[test]
fn calculate_script_string_result() {
let mut tree = FormTree::new();
make_field_with_calc(
&mut tree,
"Greeting",
"",
Some("Concat(\"Hello\", \" \", \"World\")"),
);
let result = run_calculations(&mut tree).unwrap();
assert_eq!(result.updated_fields.len(), 1);
if let FormNodeType::Field { value } = &tree.get(result.updated_fields[0]).node_type {
assert_eq!(value, "Hello World");
}
}
#[test]
fn no_update_when_value_unchanged() {
let mut tree = FormTree::new();
make_field_with_calc(&mut tree, "Same", "42", Some("42"));
let result = run_calculations(&mut tree).unwrap();
assert_eq!(result.updated_fields.len(), 0); }
#[test]
fn fields_without_scripts_are_untouched() {
let mut tree = FormTree::new();
make_field_with_calc(&mut tree, "Static", "original", None);
let result = run_calculations(&mut tree).unwrap();
assert_eq!(result.updated_fields.len(), 0);
if let FormNodeType::Field { value } = &tree.get(FormNodeId(0)).node_type {
assert_eq!(value, "original");
}
}
#[test]
fn validation_passes_for_truthy() {
let mut tree = FormTree::new();
let id = tree.add_node(FormNode {
name: "Amount".to_string(),
node_type: FormNodeType::Field {
value: "100".to_string(),
},
box_model: BoxModel {
width: Some(100.0),
height: Some(20.0),
max_width: f64::MAX,
max_height: f64::MAX,
..Default::default()
},
layout: LayoutStrategy::Positioned,
children: vec![],
occur: Occur::once(),
font: FontMetrics::default(),
calculate: None,
validate: Some("1".to_string()), column_widths: vec![],
col_span: 1,
});
let _ = id;
let result = run_validations(&tree).unwrap();
assert!(result.validation_errors.is_empty());
}
#[test]
fn validation_fails_for_falsy() {
let mut tree = FormTree::new();
tree.add_node(FormNode {
name: "Required".to_string(),
node_type: FormNodeType::Field {
value: "".to_string(),
},
box_model: BoxModel {
width: Some(100.0),
height: Some(20.0),
max_width: f64::MAX,
max_height: f64::MAX,
..Default::default()
},
layout: LayoutStrategy::Positioned,
children: vec![],
occur: Occur::once(),
font: FontMetrics::default(),
calculate: None,
validate: Some("0".to_string()), column_widths: vec![],
col_span: 1,
});
let result = run_validations(&tree).unwrap();
assert_eq!(result.validation_errors.len(), 1);
}
#[test]
fn prepare_form_runs_both() {
let mut tree = FormTree::new();
make_field_with_calc(&mut tree, "Sum", "0", Some("5 * 3"));
tree.add_node(FormNode {
name: "Check".to_string(),
node_type: FormNodeType::Field {
value: "ok".to_string(),
},
box_model: BoxModel {
width: Some(100.0),
height: Some(20.0),
max_width: f64::MAX,
max_height: f64::MAX,
..Default::default()
},
layout: LayoutStrategy::Positioned,
children: vec![],
occur: Occur::once(),
font: FontMetrics::default(),
calculate: None,
validate: Some("0".to_string()), column_widths: vec![],
col_span: 1,
});
let result = prepare_form(&mut tree).unwrap();
assert_eq!(result.updated_fields.len(), 1);
if let FormNodeType::Field { value } = &tree.get(FormNodeId(0)).node_type {
assert_eq!(value, "15");
}
assert_eq!(result.validation_errors.len(), 1);
}
#[test]
fn complex_calculation() {
let mut tree = FormTree::new();
make_field_with_calc(&mut tree, "Tax", "0", Some("Round(100 * 0.21, 2)"));
let result = run_calculations(&mut tree).unwrap();
assert_eq!(result.updated_fields.len(), 1);
if let FormNodeType::Field { value } = &tree.get(result.updated_fields[0]).node_type {
assert_eq!(value, "21");
}
}
}