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