use crate::tree::{FieldTree, FieldType, FieldValue};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FormKind {
AcroForm,
Xfa,
None,
}
#[derive(Debug, thiserror::Error)]
pub enum FormError {
#[error("field not found: {0}")]
FieldNotFound(String),
#[error("read-only field: {0}")]
ReadOnly(String),
#[error("invalid value for field type")]
InvalidValue,
}
pub trait FormAccess {
fn form_type(&self) -> FormKind;
fn field_names(&self) -> Vec<String>;
fn get_value(&self, path: &str) -> Option<String>;
fn set_value(&mut self, path: &str, value: &str) -> Result<(), FormError>;
}
pub trait DocumentOps {
fn page_count(&self) -> usize;
fn form(&self) -> Option<&dyn FormAccess>;
fn form_mut(&mut self) -> Option<&mut dyn FormAccess>;
}
impl FormAccess for FieldTree {
fn form_type(&self) -> FormKind {
FormKind::AcroForm
}
fn field_names(&self) -> Vec<String> {
self.terminal_fields()
.into_iter()
.map(|id| self.fully_qualified_name(id))
.collect()
}
fn get_value(&self, path: &str) -> Option<String> {
let id = self.find_by_name(path)?;
let value = self.effective_value(id)?;
match value {
FieldValue::Text(s) => Some(s.clone()),
FieldValue::StringArray(arr) => Some(arr.join(", ")),
}
}
fn set_value(&mut self, path: &str, value: &str) -> Result<(), FormError> {
let id = self
.find_by_name(path)
.ok_or_else(|| FormError::FieldNotFound(path.to_string()))?;
let ft = self
.effective_field_type(id)
.ok_or(FormError::InvalidValue)?;
if ft == FieldType::Signature || self.effective_flags(id).read_only() {
return Err(FormError::ReadOnly(path.to_string()));
}
match ft {
FieldType::Text | FieldType::Button => {
self.get_mut(id).value = Some(FieldValue::Text(value.to_string()));
}
FieldType::Choice => {
self.get_mut(id).value = Some(FieldValue::StringArray(vec![value.to_string()]));
}
FieldType::Signature => unreachable!(),
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::flags::FieldFlags;
use crate::tree::{FieldNode, FieldTree, FieldType, FieldValue};
fn make_node(name: &str) -> FieldNode {
FieldNode {
partial_name: name.into(),
alternate_name: None,
mapping_name: None,
field_type: None,
flags: FieldFlags::empty(),
value: None,
default_value: None,
default_appearance: None,
quadding: None,
max_len: None,
options: vec![],
top_index: None,
rect: None,
appearance_state: None,
page_index: None,
parent: None,
children: vec![],
object_id: None,
has_actions: false,
mk: None,
border_style: None,
}
}
fn sample_tree() -> FieldTree {
let mut tree = FieldTree::new();
let form_id = tree.alloc(make_node("form"));
let mut name_node = make_node("name");
name_node.field_type = Some(FieldType::Text);
name_node.value = Some(FieldValue::Text("Alice".to_string()));
name_node.parent = Some(form_id);
let name_id = tree.alloc(name_node);
let mut agree_node = make_node("agree");
agree_node.field_type = Some(FieldType::Button);
agree_node.value = Some(FieldValue::Text("true".to_string()));
agree_node.parent = Some(form_id);
let agree_id = tree.alloc(agree_node);
let mut country_node = make_node("country");
country_node.field_type = Some(FieldType::Choice);
country_node.value = Some(FieldValue::StringArray(vec!["NL".to_string()]));
country_node.parent = Some(form_id);
let country_id = tree.alloc(country_node);
let mut sig_node = make_node("sig");
sig_node.field_type = Some(FieldType::Signature);
sig_node.parent = Some(form_id);
let sig_id = tree.alloc(sig_node);
let mut empty_node = make_node("empty");
empty_node.field_type = Some(FieldType::Text);
empty_node.parent = Some(form_id);
let empty_id = tree.alloc(empty_node);
let mut locked_node = make_node("locked");
locked_node.field_type = Some(FieldType::Text);
locked_node.flags = FieldFlags::from_bits(1); locked_node.value = Some(FieldValue::Text("frozen".to_string()));
locked_node.parent = Some(form_id);
let locked_id = tree.alloc(locked_node);
let form = tree.get_mut(form_id);
form.children = vec![name_id, agree_id, country_id, sig_id, empty_id, locked_id];
tree
}
#[test]
fn field_names_returns_all() {
let tree = sample_tree();
let names = tree.field_names();
assert_eq!(names.len(), 6);
assert!(names.contains(&"form.name".to_string()));
assert!(names.contains(&"form.agree".to_string()));
}
#[test]
fn get_value_existing_text() {
let tree = sample_tree();
assert_eq!(tree.get_value("form.name"), Some("Alice".to_string()));
}
#[test]
fn get_value_returns_none_for_unknown() {
let tree = sample_tree();
assert_eq!(tree.get_value("nonexistent"), None);
}
#[test]
fn get_value_returns_none_for_empty() {
let tree = sample_tree();
assert_eq!(tree.get_value("form.empty"), None);
}
#[test]
fn set_value_updates_text() {
let mut tree = sample_tree();
tree.set_value("form.name", "Bob").unwrap();
assert_eq!(tree.get_value("form.name"), Some("Bob".to_string()));
}
#[test]
fn set_value_unknown_field_errors() {
let mut tree = sample_tree();
let err = tree.set_value("nonexistent", "x").unwrap_err();
assert!(matches!(err, FormError::FieldNotFound(_)));
}
#[test]
fn set_value_signature_errors() {
let mut tree = sample_tree();
let err = tree.set_value("form.sig", "x").unwrap_err();
assert!(matches!(err, FormError::ReadOnly(_)));
}
#[test]
fn set_value_readonly_field_errors() {
let mut tree = sample_tree();
let err = tree.set_value("form.locked", "new").unwrap_err();
assert!(matches!(err, FormError::ReadOnly(_)));
assert_eq!(tree.get_value("form.locked"), Some("frozen".to_string()));
}
#[test]
fn form_type_is_acroform() {
let tree = sample_tree();
assert_eq!(tree.form_type(), FormKind::AcroForm);
}
#[test]
fn object_safe() {
let tree = sample_tree();
let _dyn_ref: &dyn FormAccess = &tree;
assert_eq!(_dyn_ref.form_type(), FormKind::AcroForm);
}
}