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            page_index: None,
138            parent: None,
139            children: vec![],
140            object_id: None,
141            has_actions: false,
142            mk: None,
143            border_style: None,
144        }
145    }
146
147    fn sample_tree() -> FieldTree {
148        let mut tree = FieldTree::new();
149
150        // Root "form" node
151        let form_id = tree.alloc(make_node("form"));
152
153        // Text field: form.name = "Alice"
154        let mut name_node = make_node("name");
155        name_node.field_type = Some(FieldType::Text);
156        name_node.value = Some(FieldValue::Text("Alice".to_string()));
157        name_node.parent = Some(form_id);
158        let name_id = tree.alloc(name_node);
159
160        // Button field: form.agree = "true"
161        let mut agree_node = make_node("agree");
162        agree_node.field_type = Some(FieldType::Button);
163        agree_node.value = Some(FieldValue::Text("true".to_string()));
164        agree_node.parent = Some(form_id);
165        let agree_id = tree.alloc(agree_node);
166
167        // Choice field: form.country = ["NL"]
168        let mut country_node = make_node("country");
169        country_node.field_type = Some(FieldType::Choice);
170        country_node.value = Some(FieldValue::StringArray(vec!["NL".to_string()]));
171        country_node.parent = Some(form_id);
172        let country_id = tree.alloc(country_node);
173
174        // Signature field: form.sig (no value)
175        let mut sig_node = make_node("sig");
176        sig_node.field_type = Some(FieldType::Signature);
177        sig_node.parent = Some(form_id);
178        let sig_id = tree.alloc(sig_node);
179
180        // Empty text field: form.empty
181        let mut empty_node = make_node("empty");
182        empty_node.field_type = Some(FieldType::Text);
183        empty_node.parent = Some(form_id);
184        let empty_id = tree.alloc(empty_node);
185
186        // Read-only text field: form.locked
187        let mut locked_node = make_node("locked");
188        locked_node.field_type = Some(FieldType::Text);
189        locked_node.flags = FieldFlags::from_bits(1); // Bit 1 = ReadOnly
190        locked_node.value = Some(FieldValue::Text("frozen".to_string()));
191        locked_node.parent = Some(form_id);
192        let locked_id = tree.alloc(locked_node);
193
194        // Wire children
195        let form = tree.get_mut(form_id);
196        form.children = vec![name_id, agree_id, country_id, sig_id, empty_id, locked_id];
197
198        tree
199    }
200
201    #[test]
202    fn field_names_returns_all() {
203        let tree = sample_tree();
204        let names = tree.field_names();
205        assert_eq!(names.len(), 6);
206        assert!(names.contains(&"form.name".to_string()));
207        assert!(names.contains(&"form.agree".to_string()));
208    }
209
210    #[test]
211    fn get_value_existing_text() {
212        let tree = sample_tree();
213        assert_eq!(tree.get_value("form.name"), Some("Alice".to_string()));
214    }
215
216    #[test]
217    fn get_value_returns_none_for_unknown() {
218        let tree = sample_tree();
219        assert_eq!(tree.get_value("nonexistent"), None);
220    }
221
222    #[test]
223    fn get_value_returns_none_for_empty() {
224        let tree = sample_tree();
225        assert_eq!(tree.get_value("form.empty"), None);
226    }
227
228    #[test]
229    fn set_value_updates_text() {
230        let mut tree = sample_tree();
231        tree.set_value("form.name", "Bob").unwrap();
232        assert_eq!(tree.get_value("form.name"), Some("Bob".to_string()));
233    }
234
235    #[test]
236    fn set_value_unknown_field_errors() {
237        let mut tree = sample_tree();
238        let err = tree.set_value("nonexistent", "x").unwrap_err();
239        assert!(matches!(err, FormError::FieldNotFound(_)));
240    }
241
242    #[test]
243    fn set_value_signature_errors() {
244        let mut tree = sample_tree();
245        let err = tree.set_value("form.sig", "x").unwrap_err();
246        assert!(matches!(err, FormError::ReadOnly(_)));
247    }
248
249    #[test]
250    fn set_value_readonly_field_errors() {
251        let mut tree = sample_tree();
252        let err = tree.set_value("form.locked", "new").unwrap_err();
253        assert!(matches!(err, FormError::ReadOnly(_)));
254        // Value should remain unchanged.
255        assert_eq!(tree.get_value("form.locked"), Some("frozen".to_string()));
256    }
257
258    #[test]
259    fn form_type_is_acroform() {
260        let tree = sample_tree();
261        assert_eq!(tree.form_type(), FormKind::AcroForm);
262    }
263
264    #[test]
265    fn object_safe() {
266        let tree = sample_tree();
267        let _dyn_ref: &dyn FormAccess = &tree;
268        assert_eq!(_dyn_ref.form_type(), FormKind::AcroForm);
269    }
270}