Skip to main content

xfa_json/
schema.rs

1//! Schema extraction from FormTree.
2//!
3//! Generates a `FormSchema` describing the form's field structure,
4//! types, validation rules, and repetition constraints.
5
6use crate::types::{FieldSchema, FieldType, FormSchema};
7use indexmap::IndexMap;
8use xfa_layout_engine::form::{FormNodeId, FormNodeType, FormTree};
9
10/// Extract a schema from a FormTree.
11///
12/// Returns a `FormSchema` with an entry for every field and draw node,
13/// including type hints, repetition rules, and scripts.
14pub fn export_schema(tree: &FormTree, root: FormNodeId) -> FormSchema {
15    let mut fields = IndexMap::new();
16    let node = tree.get(root);
17
18    match &node.node_type {
19        FormNodeType::Root | FormNodeType::PageSet | FormNodeType::PageArea { .. } => {
20            for &child_id in &node.children {
21                walk_schema(tree, child_id, "", false, &mut fields);
22            }
23        }
24        _ => {
25            walk_schema(tree, root, "", false, &mut fields);
26        }
27    }
28
29    FormSchema { fields }
30}
31
32/// Recursively walk the tree collecting schema entries.
33fn walk_schema(
34    tree: &FormTree,
35    node_id: FormNodeId,
36    parent_path: &str,
37    parent_repeatable: bool,
38    fields: &mut IndexMap<String, FieldSchema>,
39) {
40    let node = tree.get(node_id);
41    let path = if parent_path.is_empty() {
42        node.name.clone()
43    } else {
44        format!("{}.{}", parent_path, node.name)
45    };
46
47    let is_repeatable = parent_repeatable || node.occur.is_repeating();
48
49    match &node.node_type {
50        FormNodeType::Field { value } => {
51            let field_type = infer_field_type(value);
52            fields.insert(
53                path.clone(),
54                FieldSchema {
55                    som_path: path,
56                    field_type,
57                    required: node.occur.min > 0,
58                    repeatable: parent_repeatable,
59                    max_occurrences: node.occur.max,
60                    calculate: node.calculate.clone(),
61                    validate: node.validate.clone(),
62                },
63            );
64        }
65        FormNodeType::Draw(..) | FormNodeType::Image { .. } => {
66            fields.insert(
67                path.clone(),
68                FieldSchema {
69                    som_path: path,
70                    field_type: FieldType::Static,
71                    required: false,
72                    repeatable: parent_repeatable,
73                    max_occurrences: Some(1),
74                    calculate: None,
75                    validate: None,
76                },
77            );
78        }
79        // Area, ExclGroup, and SubformSet recurse like Subform for schema export.
80        FormNodeType::Subform
81        | FormNodeType::Area
82        | FormNodeType::ExclGroup
83        | FormNodeType::SubformSet
84        | FormNodeType::Root
85        | FormNodeType::PageSet
86        | FormNodeType::PageArea { .. } => {
87            for &child_id in &node.children {
88                walk_schema(tree, child_id, &path, is_repeatable, fields);
89            }
90        }
91    }
92}
93
94/// Infer the field type from the current value.
95fn infer_field_type(value: &str) -> FieldType {
96    let trimmed = value.trim();
97
98    if trimmed.is_empty() {
99        return FieldType::Text; // Unknown, default to text
100    }
101
102    match trimmed.to_ascii_lowercase().as_str() {
103        "true" | "false" | "0" | "1" => return FieldType::Boolean,
104        _ => {}
105    }
106
107    if trimmed.parse::<f64>().is_ok() {
108        return FieldType::Numeric;
109    }
110
111    FieldType::Text
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use xfa_layout_engine::form::{FormNode, Occur};
118    use xfa_layout_engine::text::FontMetrics;
119    use xfa_layout_engine::types::{BoxModel, LayoutStrategy};
120
121    fn make_field(
122        tree: &mut FormTree,
123        name: &str,
124        value: &str,
125        calculate: Option<&str>,
126        validate: Option<&str>,
127    ) -> FormNodeId {
128        tree.add_node(FormNode {
129            name: name.to_string(),
130            node_type: FormNodeType::Field {
131                value: value.to_string(),
132            },
133            box_model: BoxModel::default(),
134            layout: LayoutStrategy::Positioned,
135            children: vec![],
136            occur: Occur::once(),
137            font: FontMetrics::default(),
138            calculate: calculate.map(|s| s.to_string()),
139            validate: validate.map(|s| s.to_string()),
140            column_widths: vec![],
141            col_span: 1,
142        })
143    }
144
145    fn make_subform(
146        tree: &mut FormTree,
147        name: &str,
148        children: Vec<FormNodeId>,
149        occur: Occur,
150    ) -> FormNodeId {
151        tree.add_node(FormNode {
152            name: name.to_string(),
153            node_type: FormNodeType::Subform,
154            box_model: BoxModel::default(),
155            layout: LayoutStrategy::TopToBottom,
156            children,
157            occur,
158            font: FontMetrics::default(),
159            calculate: None,
160            validate: None,
161            column_widths: vec![],
162            col_span: 1,
163        })
164    }
165
166    fn make_root(tree: &mut FormTree, children: Vec<FormNodeId>) -> FormNodeId {
167        tree.add_node(FormNode {
168            name: "Root".to_string(),
169            node_type: FormNodeType::Root,
170            box_model: BoxModel::default(),
171            layout: LayoutStrategy::TopToBottom,
172            children,
173            occur: Occur::once(),
174            font: FontMetrics::default(),
175            calculate: None,
176            validate: None,
177            column_widths: vec![],
178            col_span: 1,
179        })
180    }
181
182    #[test]
183    fn schema_captures_field_types() {
184        let mut tree = FormTree::new();
185        let name = make_field(&mut tree, "Name", "Acme", None, None);
186        let amount = make_field(&mut tree, "Amount", "42.50", None, None);
187        let active = make_field(&mut tree, "Active", "true", None, None);
188
189        let form = make_subform(
190            &mut tree,
191            "form1",
192            vec![name, amount, active],
193            Occur::once(),
194        );
195        let root = make_root(&mut tree, vec![form]);
196
197        let schema = export_schema(&tree, root);
198
199        assert_eq!(
200            schema.fields.get("form1.Name").unwrap().field_type,
201            FieldType::Text
202        );
203        assert_eq!(
204            schema.fields.get("form1.Amount").unwrap().field_type,
205            FieldType::Numeric
206        );
207        assert_eq!(
208            schema.fields.get("form1.Active").unwrap().field_type,
209            FieldType::Boolean
210        );
211    }
212
213    #[test]
214    fn schema_includes_scripts() {
215        let mut tree = FormTree::new();
216        let tax = make_field(
217            &mut tree,
218            "Tax",
219            "0",
220            Some("Subtotal * 0.21"),
221            Some("Tax >= 0"),
222        );
223        let form = make_subform(&mut tree, "form1", vec![tax], Occur::once());
224        let root = make_root(&mut tree, vec![form]);
225
226        let schema = export_schema(&tree, root);
227        let tax_schema = schema.fields.get("form1.Tax").unwrap();
228
229        assert_eq!(tax_schema.calculate, Some("Subtotal * 0.21".to_string()));
230        assert_eq!(tax_schema.validate, Some("Tax >= 0".to_string()));
231    }
232
233    #[test]
234    fn schema_marks_repeatable_fields() {
235        let mut tree = FormTree::new();
236        let desc = make_field(&mut tree, "Description", "Item", None, None);
237        let item = make_subform(&mut tree, "Item", vec![desc], Occur::repeating(0, None, 1));
238        let form = make_subform(&mut tree, "form1", vec![item], Occur::once());
239        let root = make_root(&mut tree, vec![form]);
240
241        let schema = export_schema(&tree, root);
242        let desc_schema = schema.fields.get("form1.Item.Description").unwrap();
243
244        assert!(desc_schema.repeatable);
245    }
246
247    #[test]
248    fn schema_required_field() {
249        let mut tree = FormTree::new();
250        let req = make_field(&mut tree, "Required", "x", None, None);
251        let form = make_subform(&mut tree, "form1", vec![req], Occur::once());
252        let root = make_root(&mut tree, vec![form]);
253
254        let schema = export_schema(&tree, root);
255        assert!(schema.fields.get("form1.Required").unwrap().required);
256    }
257
258    #[test]
259    fn infer_field_type_works() {
260        assert_eq!(infer_field_type("hello"), FieldType::Text);
261        assert_eq!(infer_field_type("42"), FieldType::Numeric);
262        assert_eq!(infer_field_type("3.14"), FieldType::Numeric);
263        assert_eq!(infer_field_type("true"), FieldType::Boolean);
264        assert_eq!(infer_field_type("0"), FieldType::Boolean);
265        assert_eq!(infer_field_type(""), FieldType::Text);
266    }
267}