Skip to main content

xfa_json/
types.rs

1//! Core JSON types for the XFA JSON-first API.
2
3// Copyright (c) 2026 Innovation Trigger B.V.
4//
5// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
6// the PDFluent Commercial Licence. See the LICENSE file in this repository --
7// that file travels with the copy you received, which a URL does not.
8
9use indexmap::IndexMap;
10use serde::{Deserialize, Serialize};
11
12/// A JSON-friendly representation of an XFA form's data.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct FormData {
15    /// Field values keyed by SOM-style dotted path.
16    pub fields: IndexMap<String, FieldValue>,
17}
18
19/// A typed form field value with automatic coercion from XFA string values.
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21#[serde(untagged)]
22pub enum FieldValue {
23    /// Numeric value (coerced from string when parseable as f64).
24    Number(f64),
25    /// Boolean value (coerced from "true"/"false"/"1"/"0").
26    Boolean(bool),
27    /// Text string value.
28    Text(String),
29    /// Null value (from empty or missing fields).
30    Null,
31    /// Repeating section: array of sub-objects.
32    Array(Vec<IndexMap<String, FieldValue>>),
33}
34
35/// Schema metadata for a form's fields.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct FormSchema {
38    /// Field schemas keyed by SOM-style dotted path.
39    pub fields: IndexMap<String, FieldSchema>,
40}
41
42/// The semantic type of a form field.
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "lowercase")]
45pub enum FieldType {
46    /// Free-text input.
47    Text,
48    /// Numeric input.
49    Numeric,
50    /// Boolean (checkbox/toggle).
51    Boolean,
52    /// Static content (Draw elements).
53    Static,
54}
55
56/// Schema for a single form field.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct FieldSchema {
59    /// Full SOM-style path to the field.
60    pub som_path: String,
61    /// Semantic field type.
62    pub field_type: FieldType,
63    /// Whether this field is required (occur.min > 0).
64    pub required: bool,
65    /// Whether the parent section can repeat.
66    pub repeatable: bool,
67    /// Maximum occurrences (None = unlimited).
68    pub max_occurrences: Option<u32>,
69    /// FormCalc calculate script, if any.
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub calculate: Option<String>,
72    /// FormCalc validate script, if any.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub validate: Option<String>,
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn field_value_json_roundtrip() {
83        let val = FieldValue::Number(42.5);
84        let json = serde_json::to_string(&val).unwrap();
85        assert_eq!(json, "42.5");
86
87        let val = FieldValue::Boolean(true);
88        let json = serde_json::to_string(&val).unwrap();
89        assert_eq!(json, "true");
90
91        let val = FieldValue::Text("hello".to_string());
92        let json = serde_json::to_string(&val).unwrap();
93        assert_eq!(json, "\"hello\"");
94
95        let val = FieldValue::Null;
96        let json = serde_json::to_string(&val).unwrap();
97        assert_eq!(json, "null");
98    }
99
100    #[test]
101    fn form_data_json_roundtrip() {
102        let mut fields = IndexMap::new();
103        fields.insert("name".to_string(), FieldValue::Text("Acme".to_string()));
104        fields.insert("amount".to_string(), FieldValue::Number(100.0));
105        let data = FormData { fields };
106
107        let json = serde_json::to_string_pretty(&data).unwrap();
108        let parsed: FormData = serde_json::from_str(&json).unwrap();
109        assert_eq!(parsed.fields.len(), 2);
110    }
111
112    #[test]
113    fn field_schema_omits_none_scripts() {
114        let schema = FieldSchema {
115            som_path: "form1.Name".to_string(),
116            field_type: FieldType::Text,
117            required: true,
118            repeatable: false,
119            max_occurrences: Some(1),
120            calculate: None,
121            validate: None,
122        };
123        let json = serde_json::to_string(&schema).unwrap();
124        assert!(!json.contains("calculate"));
125        assert!(!json.contains("validate"));
126    }
127}