fiscal_core/
tax_element.rs1#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct TaxField {
11 pub name: String,
13 pub value: String,
15}
16
17impl TaxField {
18 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
20 Self {
21 name: name.into(),
22 value: value.into(),
23 }
24 }
25}
26
27#[derive(Debug, Clone)]
29pub struct TaxElement {
30 pub outer_tag: Option<String>,
32 pub outer_fields: Vec<TaxField>,
34 pub variant_tag: String,
36 pub fields: Vec<TaxField>,
38}
39
40pub fn optional_field(name: &str, value: Option<&str>) -> Option<TaxField> {
42 value.map(|v| TaxField::new(name, v))
43}
44
45pub fn required_field(name: &str, value: Option<&str>) -> Result<TaxField, crate::FiscalError> {
51 match value {
52 Some(v) => Ok(TaxField::new(name, v)),
53 None => Err(crate::FiscalError::MissingRequiredField {
54 field: name.to_string(),
55 }),
56 }
57}
58
59pub fn filter_fields(fields: Vec<Option<TaxField>>) -> Vec<TaxField> {
61 fields.into_iter().flatten().collect()
62}
63
64fn escape_xml_value(s: &str) -> String {
66 let mut result = String::with_capacity(s.len());
67 for ch in s.chars() {
68 match ch {
69 '&' => result.push_str("&"),
70 '<' => result.push_str("<"),
71 '>' => result.push_str(">"),
72 '"' => result.push_str("""),
73 c => result.push(c),
74 }
75 }
76 result
77}
78
79fn serialize_field(field: &TaxField) -> String {
81 format!(
82 "<{name}>{value}</{name}>",
83 name = field.name,
84 value = escape_xml_value(&field.value)
85 )
86}
87
88pub fn serialize_tax_element(element: &TaxElement) -> String {
90 let inner_content: String = element.fields.iter().map(serialize_field).collect();
91 let variant_xml = format!("<{tag}>{inner_content}</{tag}>", tag = element.variant_tag,);
92
93 match &element.outer_tag {
94 None => variant_xml,
95 Some(outer) => {
96 let outer_fields_xml: String =
97 element.outer_fields.iter().map(serialize_field).collect();
98 format!("<{outer}>{outer_fields_xml}{variant_xml}</{outer}>")
99 }
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn escape_xml_value_with_ampersand_and_quotes() {
109 let result = escape_xml_value("Tom & Jerry \"cats\" <dogs>");
110 assert_eq!(result, "Tom & Jerry "cats" <dogs>");
111 }
112
113 #[test]
114 fn serialize_tax_element_no_outer_tag() {
115 let element = TaxElement {
116 outer_tag: None,
117 outer_fields: vec![],
118 variant_tag: "II".to_string(),
119 fields: vec![
120 TaxField::new("vBC", "100.00"),
121 TaxField::new("vII", "10.00"),
122 ],
123 };
124 let xml = serialize_tax_element(&element);
125 assert_eq!(xml, "<II><vBC>100.00</vBC><vII>10.00</vII></II>");
126 }
127
128 #[test]
129 fn serialize_tax_element_with_outer_tag() {
130 let element = TaxElement {
131 outer_tag: Some("IPI".to_string()),
132 outer_fields: vec![TaxField::new("cEnq", "999")],
133 variant_tag: "IPINT".to_string(),
134 fields: vec![TaxField::new("CST", "53")],
135 };
136 let xml = serialize_tax_element(&element);
137 assert!(xml.starts_with("<IPI>"));
138 assert!(xml.contains("<cEnq>999</cEnq>"));
139 assert!(xml.contains("<IPINT><CST>53</CST></IPINT>"));
140 assert!(xml.ends_with("</IPI>"));
141 }
142}