config_disassembler/xml/builders/
build_disassembled_file.rs1use crate::xml::builders::build_xml_string;
4use crate::xml::parsers::parse_unique_id_element;
5use crate::xml::transformers::transform_format;
6use crate::xml::types::BuildDisassembledFileOptions;
7use serde_json::{Map, Value};
8use std::path::Path;
9use tokio::fs;
10use tokio::io::AsyncWriteExt;
11
12pub async fn build_disassembled_file(
13 options: BuildDisassembledFileOptions<'_>,
14) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
15 let BuildDisassembledFileOptions {
16 content,
17 disassembled_path,
18 output_file_name,
19 subdirectory,
20 wrap_key,
21 is_grouped_array,
22 root_element_name,
23 root_attributes,
24 xml_declaration,
25 format,
26 unique_id_elements,
27 precomputed_unique_id,
28 } = options;
29
30 let target_directory = if let Some(subdir) = subdirectory {
31 Path::new(disassembled_path).join(subdir)
32 } else {
33 Path::new(disassembled_path).to_path_buf()
34 };
35
36 let file_name = if let Some(name) = output_file_name {
37 name.to_string()
38 } else if let Some(wk) = wrap_key {
39 if !is_grouped_array && content.is_object() {
40 let id = precomputed_unique_id
46 .map(str::to_string)
47 .unwrap_or_else(|| parse_unique_id_element(&content, unique_id_elements));
48 format!("{}.{}-meta.{}", id, wk, format)
49 } else {
50 "output".to_string()
51 }
52 } else {
53 "output".to_string()
54 };
55
56 let output_path = target_directory.join(&file_name);
57
58 fs::create_dir_all(&target_directory).await?;
59
60 let root_attrs_obj = root_attributes.as_object().cloned().unwrap_or_default();
61 let mut inner = root_attrs_obj.clone();
62
63 if let Some(wk) = wrap_key {
64 inner.insert(wk.to_string(), content.clone());
65 } else if let Some(obj) = content.as_object() {
66 for (k, v) in obj {
67 inner.insert(k.clone(), v.clone());
68 }
69 }
70
71 let mut wrapped_inner = Map::new();
72 wrapped_inner.insert(root_element_name.to_string(), Value::Object(inner));
73
74 if let Some(decl) = xml_declaration.filter(|d| d.is_object()) {
75 let mut root = Map::new();
76 root.insert("?xml".to_string(), decl);
77 for (k, v) in wrapped_inner {
78 root.insert(k, v);
79 }
80 wrapped_inner = root;
81 }
82
83 let wrapped_xml = Value::Object(wrapped_inner);
84
85 let output_string = if let Some(s) = transform_format(format, &wrapped_xml).await {
86 s
87 } else {
88 build_xml_string(&wrapped_xml)
89 };
90
91 let mut file = fs::File::create(&output_path).await?;
101 file.write_all(output_string.as_bytes()).await?;
102 file.flush().await?;
103 file.shutdown().await?;
104 log::debug!("Created disassembled file: {}", output_path.display());
105
106 Ok(())
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use serde_json::json;
113
114 fn opts_base(disassembled_path: &str) -> BuildDisassembledFileOptions<'_> {
115 BuildDisassembledFileOptions {
116 content: json!({ "a": "b" }),
117 disassembled_path,
118 output_file_name: Some("out.xml"),
119 subdirectory: None,
120 wrap_key: None,
121 is_grouped_array: false,
122 root_element_name: "Root",
123 root_attributes: Value::Object(Map::new()),
124 xml_declaration: None,
125 format: "xml",
126 unique_id_elements: None,
127 precomputed_unique_id: None,
128 }
129 }
130
131 #[tokio::test]
132 async fn build_disassembled_file_derives_name_from_unique_id_when_no_output_name() {
133 let temp = tempfile::tempdir().unwrap();
136 let path = temp.path().to_str().unwrap();
137 let mut opts = opts_base(path);
138 opts.output_file_name = None;
139 opts.wrap_key = Some("action");
140 opts.is_grouped_array = false;
141 opts.content = serde_json::json!({ "name": { "#text": "MyAction" } });
142 opts.unique_id_elements = Some("name");
143 opts.precomputed_unique_id = None; build_disassembled_file(opts).await.unwrap();
145 let mut found = false;
147 for entry in std::fs::read_dir(temp.path()).unwrap() {
148 let name = entry.unwrap().file_name().to_string_lossy().to_string();
149 if name.contains("action-meta") {
150 found = true;
151 break;
152 }
153 }
154 assert!(found, "expected a file with 'action-meta' in name");
155 }
156
157 #[tokio::test]
158 async fn build_disassembled_file_file_name_output_when_wrap_key_no_output_name_grouped_array() {
159 let temp = tempfile::tempdir().unwrap();
161 let path = temp.path().to_str().unwrap();
162 let mut opts = opts_base(path);
163 opts.output_file_name = None;
164 opts.wrap_key = Some("wrap");
165 opts.is_grouped_array = true;
166 opts.content = json!([{ "x": "1" }]);
167 build_disassembled_file(opts).await.unwrap();
168 assert!(temp.path().join("output").exists());
169 }
170
171 #[tokio::test]
172 async fn build_disassembled_file_file_name_output_when_wrap_key_content_not_object() {
173 let temp = tempfile::tempdir().unwrap();
175 let path = temp.path().to_str().unwrap();
176 let mut opts = opts_base(path);
177 opts.output_file_name = None;
178 opts.wrap_key = Some("wrap");
179 opts.is_grouped_array = false;
180 opts.content = json!([{ "id": "a" }]);
181 build_disassembled_file(opts).await.unwrap();
182 assert!(temp.path().join("output").exists());
183 }
184
185 #[tokio::test]
186 async fn build_disassembled_file_file_name_output_when_no_wrap_key_no_output_name() {
187 let temp = tempfile::tempdir().unwrap();
189 let path = temp.path().to_str().unwrap();
190 let mut opts = opts_base(path);
191 opts.output_file_name = None;
192 opts.wrap_key = None;
193 build_disassembled_file(opts).await.unwrap();
194 assert!(temp.path().join("output").exists());
195 }
196
197 #[tokio::test]
198 async fn build_disassembled_file_content_not_object_no_spread() {
199 let temp = tempfile::tempdir().unwrap();
201 let path = temp.path().to_str().unwrap();
202 let mut opts = opts_base(path);
203 opts.output_file_name = Some("single.xml");
204 opts.wrap_key = None;
205 opts.content = json!(42);
206 opts.root_attributes = json!({ "marker": "kept" });
207 build_disassembled_file(opts).await.unwrap();
208 let out = fs::read_to_string(temp.path().join("single.xml"))
209 .await
210 .unwrap();
211 assert!(out.contains("<Root"), "expected Root element, got: {out}");
212 assert!(out.contains("marker"), "expected root metadata, got: {out}");
213 assert!(
214 out.contains("kept"),
215 "expected root metadata value, got: {out}"
216 );
217 assert!(!out.contains("42"));
218 }
219}