Skip to main content

xml_disassembler/builders/
build_xml_string.rs

1//! Build XML string from XmlElement structure.
2
3use quick_xml::events::{BytesCData, BytesDecl, BytesEnd, BytesStart, BytesText, Event};
4use quick_xml::Writer;
5use serde_json::{Map, Value};
6
7use crate::types::XmlElement;
8
9fn value_to_string(v: &Value) -> String {
10    match v {
11        Value::String(s) => s.clone(),
12        Value::Number(n) => n.to_string(),
13        Value::Bool(b) => b.to_string(),
14        Value::Null => String::new(),
15        _ => serde_json::to_string(v).unwrap_or_default(),
16    }
17}
18
19fn write_element<W: std::io::Write>(
20    writer: &mut Writer<W>,
21    name: &str,
22    content: &Value,
23    indent_level: usize,
24) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
25    let indent = "    ".repeat(indent_level);
26    let child_indent = "    ".repeat(indent_level + 1);
27
28    match content {
29        Value::Object(obj) => {
30            let (attrs, children): (Vec<_>, Vec<_>) =
31                obj.iter().partition(|(k, _)| k.starts_with('@'));
32
33            let attr_name = |k: &str| k.trim_start_matches('@').to_string();
34
35            let mut text_content = String::new();
36            let mut cdata_content = String::new();
37            let child_elements: Vec<(&String, &Value)> = children
38                .iter()
39                .filter_map(|(k, v)| {
40                    if *k == "#text" {
41                        text_content = value_to_string(v);
42                        None
43                    } else if *k == "#cdata" {
44                        cdata_content = value_to_string(v);
45                        None
46                    } else {
47                        Some((*k, *v))
48                    }
49                })
50                .collect();
51
52            let has_children = child_elements.iter().any(|(_, v)| {
53                v.is_object()
54                    || (v.is_array() && v.as_array().map(|a| !a.is_empty()).unwrap_or(false))
55            });
56
57            let attrs: Vec<(String, String)> = attrs
58                .iter()
59                .map(|(k, v)| (attr_name(k), value_to_string(v)))
60                .collect();
61
62            let mut start = BytesStart::new(name);
63            for (k, v) in &attrs {
64                start.push_attribute((k.as_str(), v.as_str()));
65            }
66            writer.write_event(Event::Start(start))?;
67
68            if has_children || !child_elements.is_empty() {
69                writer.write_event(Event::Text(BytesText::new(
70                    format!("\n{}", child_indent).as_str(),
71                )))?;
72
73                let child_count = child_elements.len();
74                for (idx, (child_name, child_value)) in child_elements.iter().enumerate() {
75                    let is_last = idx == child_count - 1;
76                    match child_value {
77                        Value::Array(arr) => {
78                            let arr_len = arr.len();
79                            for (i, item) in arr.iter().enumerate() {
80                                let arr_last = i == arr_len - 1;
81                                write_element(writer, child_name, item, indent_level + 1)?;
82                                if !arr_last {
83                                    writer.write_event(Event::Text(BytesText::new(
84                                        format!("\n{}", child_indent).as_str(),
85                                    )))?;
86                                }
87                            }
88                            if !is_last {
89                                writer.write_event(Event::Text(BytesText::new(
90                                    format!("\n{}", child_indent).as_str(),
91                                )))?;
92                            }
93                        }
94                        Value::Object(_) => {
95                            write_element(writer, child_name, child_value, indent_level + 1)?;
96                            if !is_last {
97                                writer.write_event(Event::Text(BytesText::new(
98                                    format!("\n{}", child_indent).as_str(),
99                                )))?;
100                            }
101                        }
102                        _ => {
103                            writer
104                                .write_event(Event::Start(BytesStart::new(child_name.as_str())))?;
105                            // BytesText::new() expects unescaped content; the writer escapes when writing
106                            writer.write_event(Event::Text(BytesText::new(
107                                value_to_string(child_value).as_str(),
108                            )))?;
109                            writer.write_event(Event::End(BytesEnd::new(child_name.as_str())))?;
110                            if !is_last {
111                                writer.write_event(Event::Text(BytesText::new(
112                                    format!("\n{}", child_indent).as_str(),
113                                )))?;
114                            }
115                        }
116                    }
117                }
118
119                writer.write_event(Event::Text(BytesText::new(
120                    format!("\n{}", indent).as_str(),
121                )))?;
122            } else if !cdata_content.is_empty() || !text_content.is_empty() {
123                // Add newline+indent before content when no #text (keeps CDATA on separate line)
124                if text_content.is_empty() {
125                    writer.write_event(Event::Text(BytesText::new(
126                        format!("\n{}", child_indent).as_str(),
127                    )))?;
128                }
129                // Output #text first (e.g. whitespace before CDATA), then #cdata
130                if !text_content.is_empty() {
131                    writer.write_event(Event::Text(BytesText::new(text_content.as_str())))?;
132                }
133                if !cdata_content.is_empty() {
134                    writer.write_event(Event::CData(BytesCData::new(cdata_content.as_str())))?;
135                }
136                // Add newline+indent before closing tag only for CDATA (keeps compact for text-only)
137                if !cdata_content.is_empty() {
138                    writer.write_event(Event::Text(BytesText::new(
139                        format!("\n{}", indent).as_str(),
140                    )))?;
141                }
142            }
143
144            writer.write_event(Event::End(BytesEnd::new(name)))?;
145        }
146        Value::Array(arr) => {
147            for item in arr {
148                write_element(writer, name, item, indent_level)?;
149            }
150        }
151        _ => {
152            writer.write_event(Event::Start(BytesStart::new(name)))?;
153            // BytesText::new() expects unescaped content; the writer escapes when writing
154            writer.write_event(Event::Text(BytesText::new(
155                value_to_string(content).as_str(),
156            )))?;
157            writer.write_event(Event::End(BytesEnd::new(name)))?;
158        }
159    }
160
161    Ok(())
162}
163
164fn build_xml_from_object(
165    element: &Map<String, Value>,
166) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
167    // Use Writer::new (no indent) so leaf elements stay compact and match fixture format
168    let mut writer = Writer::new(Vec::new());
169
170    let (declaration, root_key, root_value) = if let Some(decl) = element.get("?xml") {
171        let root_key = element
172            .keys()
173            .find(|k| *k != "?xml")
174            .cloned()
175            .unwrap_or_else(|| "root".to_string());
176        let root_value = element
177            .get(&root_key)
178            .cloned()
179            .unwrap_or_else(|| Value::Object(Map::new()));
180        (Some(decl), root_key, root_value)
181    } else {
182        let root_key = element
183            .keys()
184            .next()
185            .cloned()
186            .unwrap_or_else(|| "root".to_string());
187        let root_value = element
188            .get(&root_key)
189            .cloned()
190            .unwrap_or_else(|| Value::Object(Map::new()));
191        (None, root_key, root_value)
192    };
193
194    if declaration.is_some() {
195        if let Some(decl) = declaration {
196            if let Some(obj) = decl.as_object() {
197                let version = obj
198                    .get("@version")
199                    .and_then(|v| v.as_str())
200                    .unwrap_or("1.0");
201                let encoding = obj.get("@encoding").and_then(|v| v.as_str());
202                let standalone = obj.get("@standalone").and_then(|v| v.as_str());
203                writer.write_event(Event::Decl(BytesDecl::new(version, encoding, standalone)))?;
204                writer.write_event(Event::Text(BytesText::new("\n")))?;
205            }
206        }
207    }
208
209    write_element(&mut writer, &root_key, &root_value, 0)?;
210
211    let result = String::from_utf8(writer.into_inner())?;
212    Ok(result.trim_end().to_string())
213}
214
215/// Build XML string from XmlElement.
216pub fn build_xml_string(element: &XmlElement) -> String {
217    match element {
218        Value::Object(obj) => build_xml_from_object(obj).unwrap_or_default(),
219        _ => String::new(),
220    }
221}