use std::collections::{BTreeMap, BTreeSet};
use crate::eval::{evaluate, Context, Value};
use crate::rules::{Clock, Form, Violation};
use crate::tree::{Instance, NodeId};
pub struct Session {
form: Form,
instance: Instance,
clock: Clock,
templates: BTreeMap<String, Instance>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Outcome {
pub calculated: BTreeMap<String, String>,
pub relevant: BTreeMap<String, bool>,
pub missing: Vec<String>,
pub invalid: Vec<(String, String)>,
pub failed: Vec<(String, String)>,
pub repeats: BTreeMap<String, usize>,
}
impl Session {
pub fn new(xform: &str, clock: Clock) -> Result<Self, String> {
let form = Form::parse(xform)?;
let mut instance = blank_instance(xform)?;
let templates = lift_templates(&mut instance);
Ok(Session {
form,
instance,
clock,
templates,
})
}
pub fn resume(xform: &str, instance_xml: &str, clock: Clock) -> Result<Self, String> {
let form = Form::parse(xform)?;
let mut instance =
Instance::from_xml(instance_xml).map_err(|e| format!("saved instance: {e}"))?;
let mut blank = blank_instance(xform)?;
let templates = lift_templates(&mut blank);
let _ = lift_templates(&mut instance);
Ok(Session {
form,
instance,
clock,
templates,
})
}
pub fn set(&mut self, path: &str, value: &str) -> Result<(), String> {
match self.node_at(path) {
Some(node) => {
self.instance.node_mut(node).value = value.to_string();
Ok(())
}
None => self.create_at(path, value),
}
}
pub fn get(&self, path: &str) -> String {
self.node_at(path)
.map(|node| self.instance.string_value(node))
.unwrap_or_default()
}
pub fn recompute(&mut self) -> Outcome {
let mut outcome = Outcome::default();
let (computed, failed) = self.form.calculations(&self.instance, &self.clock);
outcome.failed = failed;
for (path, value) in computed {
if self.get(&path) != value {
let _ = self.set(&path, &value);
outcome.calculated.insert(path, value);
}
}
let (relevance, problems) = self.form.relevance(&self.instance, &self.clock);
outcome.relevant = relevance;
for problem in problems {
outcome
.failed
.push((problem.node_path.clone(), problem.describe()));
}
let hidden: BTreeSet<String> = outcome
.relevant
.iter()
.filter(|(_, shown)| !**shown)
.map(|(path, _)| path.clone())
.collect();
for violation in self.form.check(&self.instance, self.clock.clone()) {
let path = violation.node_path.clone();
if hidden.contains(&path) {
continue;
}
use crate::rules::ViolationKind::*;
match &violation.kind {
Required => outcome.missing.push(path),
Constraint => outcome.invalid.push((
path,
violation
.message
.clone()
.unwrap_or_else(|| "this answer is not allowed".into()),
)),
Failed(why) => outcome.failed.push((path, why.clone())),
Calculation { .. } => {}
}
}
outcome.repeats = self.repeat_counts();
outcome
}
pub fn repeat_counts(&self) -> BTreeMap<String, usize> {
let mut counts = BTreeMap::new();
for path in self.templates.keys() {
counts.insert(path.clone(), self.rows_of(path).len());
}
counts
}
pub fn add_row(&mut self, path: &str) -> Result<usize, String> {
let template = self
.templates
.get(path)
.ok_or_else(|| format!("'{path}' is not a repeat in this form"))?
.clone();
let template_root = template
.root()
.ok_or_else(|| format!("the template for '{path}' is empty"))?;
let rows = self.rows_of(path);
let copied = self.instance.adopt(&template, template_root);
match rows.last() {
Some(last) => self.instance.insert_after(*last, copied),
None => {
let parent_path = path.rsplit_once('/').map(|(head, _)| head).unwrap_or("");
let parent = match parent_path {
"" => self.instance.root(),
other => self.node_at(other),
}
.ok_or_else(|| format!("'{path}' has nowhere to hang"))?;
self.instance.append_child(parent, copied);
}
}
self.instance.reindex();
Ok(self.rows_of(path).len())
}
pub fn remove_row(&mut self, path: &str, position: usize) -> Result<usize, String> {
let rows = self.rows_of(path);
if position == 0 || position > rows.len() {
return Err(format!(
"'{path}' has {} row(s); there is no row {position}",
rows.len()
));
}
self.instance.detach(rows[position - 1]);
self.instance.reindex();
Ok(self.rows_of(path).len())
}
fn rows_of(&self, path: &str) -> Vec<NodeId> {
let Some((parent_path, name)) = path.rsplit_once('/') else {
return Vec::new();
};
let parent = match parent_path {
"" => self.instance.root(),
other => self.node_at(other),
};
let Some(parent) = parent else {
return Vec::new();
};
self.instance
.children(parent)
.into_iter()
.filter(|child| self.instance.node(*child).name == name)
.collect()
}
pub fn choices(&self, path: &str) -> Option<Vec<(String, String)>> {
self.form.choices(&self.instance, path)
}
pub fn instance_xml(&self) -> String {
let mut out = String::from("<?xml version='1.0' ?>");
if let Some(root) = self.instance.root() {
write_element(&self.instance, root, &mut out);
}
out
}
pub fn check_all(&self) -> Vec<Violation> {
self.form.check(&self.instance, self.clock.clone())
}
fn node_at(&self, path: &str) -> Option<NodeId> {
let expr = crate::parser::parse(path).ok()?;
let root = self.instance.root()?;
let env = crate::eval::Fixed {
today: self.clock.today.clone(),
now: self.clock.now.clone(),
};
match evaluate(&expr, &self.instance, Context::at(root), &env) {
Ok(Value::NodeSet(nodes)) => nodes.first().copied(),
_ => None,
}
}
fn create_at(&mut self, path: &str, value: &str) -> Result<(), String> {
let mut parts: Vec<&str> = path.split('/').filter(|p| !p.is_empty()).collect();
if parts.is_empty() {
return Err(format!("'{path}' names nothing"));
}
if let Some(indexed) = parts.iter().find(|part| part.contains('[')) {
return Err(format!(
"'{path}' points at {indexed}, which does not exist — add the row first \
with add_row()"
));
}
let leaf = parts.pop().expect("checked above");
let mut here = self
.instance
.root()
.ok_or_else(|| "the instance has no root".to_string())?;
if !parts.is_empty() && self.instance.node(here).name == parts[0] {
parts.remove(0);
}
for part in parts {
let existing = self
.instance
.children(here)
.into_iter()
.find(|child| self.instance.node(*child).name == part);
here = match existing {
Some(child) => child,
None => {
let child = self.instance.create_element(part, "");
self.instance.append_child(here, child);
child
}
};
}
let leaf_node = self.instance.create_element(leaf, value);
self.instance.append_child(here, leaf_node);
self.instance.reindex();
Ok(())
}
}
fn blank_instance(xform: &str) -> Result<Instance, String> {
let document = Instance::from_xml(xform).map_err(|e| format!("XForm XML: {e}"))?;
let root = document.root().ok_or("the XForm has no root element")?;
let primary = document
.descendants(root)
.into_iter()
.find(|node| document.node(*node).name == "instance")
.ok_or("the XForm has no primary instance")?;
let template = document
.children(primary)
.into_iter()
.next()
.ok_or("the XForm's primary instance is empty")?;
let mut instance = Instance::new();
let copied = instance.adopt(&document, template);
instance.set_root(copied);
instance.reindex();
Ok(instance)
}
fn write_element(instance: &Instance, node: NodeId, out: &mut String) {
let name = &instance.node(node).name;
out.push('<');
out.push_str(name);
for attribute in instance.attributes(node) {
out.push(' ');
out.push_str(&instance.node(attribute).name);
out.push_str("=\"");
escape_into(&instance.node(attribute).value, out);
out.push('"');
}
let children = instance.children(node);
if children.is_empty() {
let value = &instance.node(node).value;
if value.is_empty() {
out.push_str("/>");
return;
}
out.push('>');
escape_into(value, out);
} else {
out.push('>');
for child in children {
write_element(instance, child, out);
}
}
out.push_str("</");
out.push_str(name);
out.push('>');
}
fn escape_into(text: &str, out: &mut String) {
for c in text.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
_ => out.push(c),
}
}
}
fn lift_templates(instance: &mut Instance) -> BTreeMap<String, Instance> {
let mut found = BTreeMap::new();
let Some(root) = instance.root() else {
return found;
};
let templates: Vec<NodeId> = instance
.descendants(root)
.into_iter()
.filter(|node| {
instance
.attributes(*node)
.into_iter()
.any(|a| instance.node(a).name == "template")
})
.collect();
for node in templates {
let path = instance.path_of(node);
let path = match path.rfind('[') {
Some(at) if path.ends_with(']') => path[..at].to_string(),
_ => path,
};
let mut lifted = Instance::new();
let copied = lifted.adopt(instance, node);
lifted.set_root(copied);
let markers: Vec<NodeId> = lifted
.attributes(copied)
.into_iter()
.filter(|a| lifted.node(*a).name == "template")
.collect();
lifted
.node_mut(copied)
.attributes
.retain(|a| !markers.contains(a));
found.insert(path, lifted);
instance.detach(node);
}
instance.reindex();
found
}