Skip to main content

pdfluent_forms/
facade.rs

1//! Unified form access facade for language bindings.
2//!
3//! Provides [`FormAccess`] and [`DocumentOps`] traits that abstract over
4//! AcroForm and XFA form technologies.  Language bindings (C, Python, WASM,
5//! Node.js) wrap these traits instead of individual crate APIs.
6
7use crate::tree::{FieldTree, FieldType, FieldValue};
8
9/// The kind of forms in a PDF document.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum FormKind {
12    /// AcroForm interactive forms (ISO 32000 §12.7).
13    AcroForm,
14    /// XFA forms (XML Forms Architecture).
15    Xfa,
16    /// No forms present.
17    None,
18}
19
20/// Error type for form operations.
21#[derive(Debug, thiserror::Error)]
22pub enum FormError {
23    /// The requested field was not found.
24    #[error("field not found: {0}")]
25    FieldNotFound(String),
26    /// The field is read-only and cannot be modified.
27    #[error("read-only field: {0}")]
28    ReadOnly(String),
29    /// The provided value is invalid for the field type.
30    #[error("invalid value for field type")]
31    InvalidValue,
32}
33
34/// Unified form field access — works for AcroForm or XFA.
35///
36/// This trait provides a common interface for reading and writing form field
37/// values regardless of the underlying form technology.
38pub trait FormAccess {
39    /// Returns the kind of form (AcroForm, XFA, or None).
40    fn form_type(&self) -> FormKind;
41
42    /// Returns all fully-qualified field names in the form.
43    fn field_names(&self) -> Vec<String>;
44
45    /// Gets the current value of a field by its fully-qualified name.
46    fn get_value(&self, path: &str) -> Option<String>;
47
48    /// Sets the value of a field by its fully-qualified name.
49    fn set_value(&mut self, path: &str, value: &str) -> Result<(), FormError>;
50}
51
52/// Unified document operations.
53///
54/// Provides access to form data, page count, and other document-level
55/// operations through a single interface.
56pub trait DocumentOps {
57    /// Returns the number of pages in the document.
58    fn page_count(&self) -> usize;
59
60    /// Returns read-only access to the form engine, if any.
61    fn form(&self) -> Option<&dyn FormAccess>;
62
63    /// Returns mutable access to the form engine, if any.
64    fn form_mut(&mut self) -> Option<&mut dyn FormAccess>;
65}
66
67impl FormAccess for FieldTree {
68    fn form_type(&self) -> FormKind {
69        FormKind::AcroForm
70    }
71
72    fn field_names(&self) -> Vec<String> {
73        self.terminal_fields()
74            .into_iter()
75            .map(|id| self.fully_qualified_name(id))
76            .collect()
77    }
78
79    fn get_value(&self, path: &str) -> Option<String> {
80        let id = self.find_by_name(path)?;
81        let value = self.effective_value(id)?;
82        match value {
83            FieldValue::Text(s) => Some(s.clone()),
84            FieldValue::StringArray(arr) => Some(arr.join(", ")),
85        }
86    }
87
88    fn set_value(&mut self, path: &str, value: &str) -> Result<(), FormError> {
89        let id = self
90            .find_by_name(path)
91            .ok_or_else(|| FormError::FieldNotFound(path.to_string()))?;
92
93        let ft = self
94            .effective_field_type(id)
95            .ok_or(FormError::InvalidValue)?;
96
97        // Reject writes to read-only fields (Ff bit 1) and signatures.
98        if ft == FieldType::Signature || self.effective_flags(id).read_only() {
99            return Err(FormError::ReadOnly(path.to_string()));
100        }
101
102        match ft {
103            FieldType::Text | FieldType::Button => {
104                self.get_mut(id).value = Some(FieldValue::Text(value.to_string()));
105            }
106            FieldType::Choice => {
107                self.get_mut(id).value = Some(FieldValue::StringArray(vec![value.to_string()]));
108            }
109            FieldType::Signature => unreachable!(),
110        }
111        Ok(())
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::flags::FieldFlags;
119    use crate::tree::{FieldNode, FieldTree, FieldType, FieldValue};
120
121    fn make_node(name: &str) -> FieldNode {
122        FieldNode {
123            partial_name: name.into(),
124            alternate_name: None,
125            mapping_name: None,
126            field_type: None,
127            flags: FieldFlags::empty(),
128            value: None,
129            default_value: None,
130            default_appearance: None,
131            quadding: None,
132            max_len: None,
133            options: vec![],
134            top_index: None,
135            rect: None,
136            appearance_state: None,
137            on_state: None,
138            page_index: None,
139            parent: None,
140            children: vec![],
141            object_id: None,
142            has_actions: false,
143            mk: None,
144            border_style: None,
145        }
146    }
147
148    fn sample_tree() -> FieldTree {
149        let mut tree = FieldTree::new();
150
151        // Root "form" node
152        let form_id = tree.alloc(make_node("form"));
153
154        // Text field: form.name = "Alice"
155        let mut name_node = make_node("name");
156        name_node.field_type = Some(FieldType::Text);
157        name_node.value = Some(FieldValue::Text("Alice".to_string()));
158        name_node.parent = Some(form_id);
159        let name_id = tree.alloc(name_node);
160
161        // Button field: form.agree = "true"
162        let mut agree_node = make_node("agree");
163        agree_node.field_type = Some(FieldType::Button);
164        agree_node.value = Some(FieldValue::Text("true".to_string()));
165        agree_node.parent = Some(form_id);
166        let agree_id = tree.alloc(agree_node);
167
168        // Choice field: form.country = ["NL"]
169        let mut country_node = make_node("country");
170        country_node.field_type = Some(FieldType::Choice);
171        country_node.value = Some(FieldValue::StringArray(vec!["NL".to_string()]));
172        country_node.parent = Some(form_id);
173        let country_id = tree.alloc(country_node);
174
175        // Signature field: form.sig (no value)
176        let mut sig_node = make_node("sig");
177        sig_node.field_type = Some(FieldType::Signature);
178        sig_node.parent = Some(form_id);
179        let sig_id = tree.alloc(sig_node);
180
181        // Empty text field: form.empty
182        let mut empty_node = make_node("empty");
183        empty_node.field_type = Some(FieldType::Text);
184        empty_node.parent = Some(form_id);
185        let empty_id = tree.alloc(empty_node);
186
187        // Read-only text field: form.locked
188        let mut locked_node = make_node("locked");
189        locked_node.field_type = Some(FieldType::Text);
190        locked_node.flags = FieldFlags::from_bits(1); // Bit 1 = ReadOnly
191        locked_node.value = Some(FieldValue::Text("frozen".to_string()));
192        locked_node.parent = Some(form_id);
193        let locked_id = tree.alloc(locked_node);
194
195        // Wire children
196        let form = tree.get_mut(form_id);
197        form.children = vec![name_id, agree_id, country_id, sig_id, empty_id, locked_id];
198
199        tree
200    }
201
202    #[test]
203    fn field_names_returns_all() {
204        let tree = sample_tree();
205        let names = tree.field_names();
206        assert_eq!(names.len(), 6);
207        assert!(names.contains(&"form.name".to_string()));
208        assert!(names.contains(&"form.agree".to_string()));
209    }
210
211    #[test]
212    fn get_value_existing_text() {
213        let tree = sample_tree();
214        assert_eq!(tree.get_value("form.name"), Some("Alice".to_string()));
215    }
216
217    #[test]
218    fn get_value_returns_none_for_unknown() {
219        let tree = sample_tree();
220        assert_eq!(tree.get_value("nonexistent"), None);
221    }
222
223    #[test]
224    fn get_value_returns_none_for_empty() {
225        let tree = sample_tree();
226        assert_eq!(tree.get_value("form.empty"), None);
227    }
228
229    #[test]
230    fn set_value_updates_text() {
231        let mut tree = sample_tree();
232        tree.set_value("form.name", "Bob").unwrap();
233        assert_eq!(tree.get_value("form.name"), Some("Bob".to_string()));
234    }
235
236    #[test]
237    fn set_value_unknown_field_errors() {
238        let mut tree = sample_tree();
239        let err = tree.set_value("nonexistent", "x").unwrap_err();
240        assert!(matches!(err, FormError::FieldNotFound(_)));
241    }
242
243    #[test]
244    fn set_value_signature_errors() {
245        let mut tree = sample_tree();
246        let err = tree.set_value("form.sig", "x").unwrap_err();
247        assert!(matches!(err, FormError::ReadOnly(_)));
248    }
249
250    #[test]
251    fn set_value_readonly_field_errors() {
252        let mut tree = sample_tree();
253        let err = tree.set_value("form.locked", "new").unwrap_err();
254        assert!(matches!(err, FormError::ReadOnly(_)));
255        // Value should remain unchanged.
256        assert_eq!(tree.get_value("form.locked"), Some("frozen".to_string()));
257    }
258
259    #[test]
260    fn form_type_is_acroform() {
261        let tree = sample_tree();
262        assert_eq!(tree.form_type(), FormKind::AcroForm);
263    }
264
265    #[test]
266    fn object_safe() {
267        let tree = sample_tree();
268        let _dyn_ref: &dyn FormAccess = &tree;
269        assert_eq!(_dyn_ref.form_type(), FormKind::AcroForm);
270    }
271}