Skip to main content

xfa_json/
export.rs

1//! Export FormTree to JSON.
2//!
3//! Walks the FormTree recursively and produces a flat `FormData` with
4//! SOM-style dotted paths as keys and coerced typed values.
5
6// Copyright (c) 2026 Innovation Trigger B.V.
7//
8// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
9// the PDFluent Commercial Licence. See the LICENSE file in this repository --
10// that file travels with the copy you received, which a URL does not.
11
12use crate::coerce::coerce_value;
13use crate::types::{FieldValue, FormData};
14use indexmap::IndexMap;
15use xfa_layout_engine::form::{DrawContent, FormNodeId, FormNodeType, FormTree};
16
17/// Convert a FormTree into a JSON-friendly `FormData` structure.
18///
19/// Walks the tree starting at `root`, collecting field values keyed
20/// by their SOM-style dotted path (e.g., `"form1.Customer.Name"`).
21/// Repeating subforms become arrays.
22pub fn form_tree_to_json(tree: &FormTree, root: FormNodeId) -> FormData {
23    let mut fields = IndexMap::new();
24    let node = tree.get(root);
25
26    // Skip root-level structural nodes and dive into content
27    match &node.node_type {
28        FormNodeType::Root | FormNodeType::PageSet | FormNodeType::PageArea { .. } => {
29            for &child_id in &node.children {
30                walk_node(tree, child_id, "", &mut fields);
31            }
32        }
33        _ => {
34            walk_node(tree, root, "", &mut fields);
35        }
36    }
37
38    FormData { fields }
39}
40
41/// Convert a FormTree into a raw `serde_json::Value`.
42///
43/// Convenience wrapper that serializes `FormData` directly.
44pub fn form_tree_to_value(tree: &FormTree, root: FormNodeId) -> serde_json::Value {
45    let data = form_tree_to_json(tree, root);
46    serde_json::to_value(data).unwrap_or(serde_json::Value::Null)
47}
48
49/// Recursively walk the FormTree, collecting fields into the flat map.
50fn walk_node(
51    tree: &FormTree,
52    node_id: FormNodeId,
53    parent_path: &str,
54    fields: &mut IndexMap<String, FieldValue>,
55) {
56    let node = tree.get(node_id);
57    let path = if parent_path.is_empty() {
58        node.name.clone()
59    } else {
60        format!("{}.{}", parent_path, node.name)
61    };
62
63    match &node.node_type {
64        FormNodeType::Field { value } => {
65            fields.insert(path, coerce_value(value));
66        }
67        FormNodeType::Draw(DrawContent::Text(content)) => {
68            if !content.is_empty() {
69                fields.insert(path, FieldValue::Text(content.clone()));
70            }
71        }
72        FormNodeType::Draw(_) | FormNodeType::Image { .. } => {
73            // Non-text draws and images are static content - not exported as form data
74        }
75        // Area and ExclGroup behave like Subform for data export.
76        // SubformSet is transparent — recurse into children.
77        FormNodeType::Subform | FormNodeType::Area | FormNodeType::ExclGroup => {
78            if node.occur.is_repeating() {
79                // Repeating subform: collect siblings with the same name as an array.
80                // The caller handles this via collect_repeating_siblings.
81                // For a single instance in a repeating group, still emit as array.
82                let mut instance = IndexMap::new();
83                for &child_id in &node.children {
84                    walk_node_into_map(tree, child_id, &mut instance);
85                }
86                // Merge into existing array if present, or create new one
87                match fields.get_mut(&path) {
88                    Some(FieldValue::Array(arr)) => {
89                        arr.push(instance);
90                    }
91                    _ => {
92                        fields.insert(path, FieldValue::Array(vec![instance]));
93                    }
94                }
95            } else {
96                // Non-repeating subform: recurse with extended path
97                for &child_id in &node.children {
98                    walk_node(tree, child_id, &path, fields);
99                }
100            }
101        }
102        FormNodeType::SubformSet
103        | FormNodeType::Root
104        | FormNodeType::PageSet
105        | FormNodeType::PageArea { .. } => {
106            for &child_id in &node.children {
107                walk_node(tree, child_id, &path, fields);
108            }
109        }
110    }
111}
112
113/// Walk a node into a flat map (used for repeating section instances).
114fn walk_node_into_map(
115    tree: &FormTree,
116    node_id: FormNodeId,
117    map: &mut IndexMap<String, FieldValue>,
118) {
119    let node = tree.get(node_id);
120
121    match &node.node_type {
122        FormNodeType::Field { value } => {
123            map.insert(node.name.clone(), coerce_value(value));
124        }
125        FormNodeType::Draw(DrawContent::Text(content)) => {
126            if !content.is_empty() {
127                map.insert(node.name.clone(), FieldValue::Text(content.clone()));
128            }
129        }
130        FormNodeType::Draw(_) | FormNodeType::Image { .. } => {
131            // Non-text draws and images are static content
132        }
133        FormNodeType::Subform => {
134            if node.occur.is_repeating() {
135                let mut instance = IndexMap::new();
136                for &child_id in &node.children {
137                    walk_node_into_map(tree, child_id, &mut instance);
138                }
139                match map.get_mut(&node.name) {
140                    Some(FieldValue::Array(arr)) => {
141                        arr.push(instance);
142                    }
143                    _ => {
144                        map.insert(node.name.clone(), FieldValue::Array(vec![instance]));
145                    }
146                }
147            } else {
148                // Nested non-repeating: prefix with subform name
149                for &child_id in &node.children {
150                    let child = tree.get(child_id);
151                    let key = format!("{}.{}", node.name, child.name);
152                    match &child.node_type {
153                        FormNodeType::Field { value } => {
154                            map.insert(key, coerce_value(value));
155                        }
156                        FormNodeType::Draw(DrawContent::Text(content)) => {
157                            if !content.is_empty() {
158                                map.insert(key, FieldValue::Text(content.clone()));
159                            }
160                        }
161                        _ => {
162                            walk_node_into_map(tree, child_id, map);
163                        }
164                    }
165                }
166            }
167        }
168        _ => {
169            for &child_id in &node.children {
170                walk_node_into_map(tree, child_id, map);
171            }
172        }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use xfa_layout_engine::form::{FormNode, Occur};
180    use xfa_layout_engine::text::FontMetrics;
181    use xfa_layout_engine::types::{BoxModel, LayoutStrategy};
182
183    fn make_field(tree: &mut FormTree, name: &str, value: &str) -> FormNodeId {
184        tree.add_node(FormNode {
185            name: name.to_string(),
186            node_type: FormNodeType::Field {
187                value: value.to_string(),
188            },
189            box_model: BoxModel::default(),
190            layout: LayoutStrategy::Positioned,
191            children: vec![],
192            occur: Occur::once(),
193            font: FontMetrics::default(),
194            calculate: None,
195            validate: None,
196            column_widths: vec![],
197            col_span: 1,
198        })
199    }
200
201    fn make_subform(
202        tree: &mut FormTree,
203        name: &str,
204        children: Vec<FormNodeId>,
205        occur: Occur,
206    ) -> FormNodeId {
207        tree.add_node(FormNode {
208            name: name.to_string(),
209            node_type: FormNodeType::Subform,
210            box_model: BoxModel::default(),
211            layout: LayoutStrategy::TopToBottom,
212            children,
213            occur,
214            font: FontMetrics::default(),
215            calculate: None,
216            validate: None,
217            column_widths: vec![],
218            col_span: 1,
219        })
220    }
221
222    fn make_root(tree: &mut FormTree, children: Vec<FormNodeId>) -> FormNodeId {
223        tree.add_node(FormNode {
224            name: "Root".to_string(),
225            node_type: FormNodeType::Root,
226            box_model: BoxModel::default(),
227            layout: LayoutStrategy::TopToBottom,
228            children,
229            occur: Occur::once(),
230            font: FontMetrics::default(),
231            calculate: None,
232            validate: None,
233            column_widths: vec![],
234            col_span: 1,
235        })
236    }
237
238    #[test]
239    fn simple_form_export() {
240        let mut tree = FormTree::new();
241        let name = make_field(&mut tree, "Name", "Acme Corp");
242        let amount = make_field(&mut tree, "Amount", "42.50");
243        let active = make_field(&mut tree, "Active", "true");
244
245        let form = make_subform(
246            &mut tree,
247            "form1",
248            vec![name, amount, active],
249            Occur::once(),
250        );
251        let root = make_root(&mut tree, vec![form]);
252
253        let data = form_tree_to_json(&tree, root);
254
255        assert_eq!(
256            data.fields.get("form1.Name"),
257            Some(&FieldValue::Text("Acme Corp".to_string()))
258        );
259        assert_eq!(
260            data.fields.get("form1.Amount"),
261            Some(&FieldValue::Number(42.50))
262        );
263        assert_eq!(
264            data.fields.get("form1.Active"),
265            Some(&FieldValue::Boolean(true))
266        );
267    }
268
269    #[test]
270    fn nested_subforms() {
271        let mut tree = FormTree::new();
272        let street = make_field(&mut tree, "Street", "123 Main St");
273        let city = make_field(&mut tree, "City", "Springfield");
274
275        let address = make_subform(&mut tree, "Address", vec![street, city], Occur::once());
276        let form = make_subform(&mut tree, "form1", vec![address], Occur::once());
277        let root = make_root(&mut tree, vec![form]);
278
279        let data = form_tree_to_json(&tree, root);
280
281        assert_eq!(
282            data.fields.get("form1.Address.Street"),
283            Some(&FieldValue::Text("123 Main St".to_string()))
284        );
285        assert_eq!(
286            data.fields.get("form1.Address.City"),
287            Some(&FieldValue::Text("Springfield".to_string()))
288        );
289    }
290
291    #[test]
292    fn repeating_subforms_as_arrays() {
293        let mut tree = FormTree::new();
294
295        let desc1 = make_field(&mut tree, "Description", "Widget A");
296        let qty1 = make_field(&mut tree, "Qty", "10");
297        let item1 = make_subform(
298            &mut tree,
299            "Item",
300            vec![desc1, qty1],
301            Occur::repeating(0, None, 2),
302        );
303
304        let desc2 = make_field(&mut tree, "Description", "Widget B");
305        let qty2 = make_field(&mut tree, "Qty", "5");
306        let item2 = make_subform(
307            &mut tree,
308            "Item",
309            vec![desc2, qty2],
310            Occur::repeating(0, None, 2),
311        );
312
313        let form = make_subform(&mut tree, "form1", vec![item1, item2], Occur::once());
314        let root = make_root(&mut tree, vec![form]);
315
316        let data = form_tree_to_json(&tree, root);
317
318        let items = data.fields.get("form1.Item").unwrap();
319        match items {
320            FieldValue::Array(arr) => {
321                assert_eq!(arr.len(), 2);
322                assert_eq!(
323                    arr[0].get("Description"),
324                    Some(&FieldValue::Text("Widget A".to_string()))
325                );
326                assert_eq!(arr[0].get("Qty"), Some(&FieldValue::Number(10.0)));
327                assert_eq!(
328                    arr[1].get("Description"),
329                    Some(&FieldValue::Text("Widget B".to_string()))
330                );
331                assert_eq!(arr[1].get("Qty"), Some(&FieldValue::Number(5.0)));
332            }
333            _ => panic!("Expected Array, got {items:?}"),
334        }
335    }
336
337    #[test]
338    fn empty_fields_are_null() {
339        let mut tree = FormTree::new();
340        let empty = make_field(&mut tree, "Empty", "");
341        let form = make_subform(&mut tree, "form1", vec![empty], Occur::once());
342        let root = make_root(&mut tree, vec![form]);
343
344        let data = form_tree_to_json(&tree, root);
345        assert_eq!(data.fields.get("form1.Empty"), Some(&FieldValue::Null));
346    }
347
348    #[test]
349    fn form_tree_to_value_produces_valid_json() {
350        let mut tree = FormTree::new();
351        let name = make_field(&mut tree, "Name", "Test");
352        let form = make_subform(&mut tree, "form1", vec![name], Occur::once());
353        let root = make_root(&mut tree, vec![form]);
354
355        let value = form_tree_to_value(&tree, root);
356        assert!(value.is_object());
357
358        let fields = value.get("fields").unwrap();
359        assert_eq!(fields.get("form1.Name").unwrap(), "Test");
360    }
361
362    #[test]
363    fn type_coercion_in_export() {
364        let mut tree = FormTree::new();
365        let num = make_field(&mut tree, "Total", "112.50");
366        let flag = make_field(&mut tree, "Checked", "0");
367        let text = make_field(&mut tree, "Note", "See attached");
368        let null_field = make_field(&mut tree, "Blank", "");
369
370        let form = make_subform(
371            &mut tree,
372            "form1",
373            vec![num, flag, text, null_field],
374            Occur::once(),
375        );
376        let root = make_root(&mut tree, vec![form]);
377
378        let data = form_tree_to_json(&tree, root);
379
380        assert_eq!(
381            data.fields.get("form1.Total"),
382            Some(&FieldValue::Number(112.50))
383        );
384        assert_eq!(
385            data.fields.get("form1.Checked"),
386            Some(&FieldValue::Boolean(false))
387        );
388        assert_eq!(
389            data.fields.get("form1.Note"),
390            Some(&FieldValue::Text("See attached".to_string()))
391        );
392        assert_eq!(data.fields.get("form1.Blank"), Some(&FieldValue::Null));
393    }
394}