Skip to main content

xml_disassembler/builders/
build_disassembled_file.rs

1//! Build a single disassembled file.
2
3use crate::builders::build_xml_string;
4use crate::parsers::parse_unique_id_element;
5use crate::transformers::transform_format;
6use crate::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    } = options;
28
29    let target_directory = if let Some(subdir) = subdirectory {
30        Path::new(disassembled_path).join(subdir)
31    } else {
32        Path::new(disassembled_path).to_path_buf()
33    };
34
35    let file_name = if let Some(name) = output_file_name {
36        name.to_string()
37    } else if let Some(wk) = wrap_key {
38        if !is_grouped_array && content.is_object() {
39            let id = parse_unique_id_element(&content, unique_id_elements);
40            format!("{}.{}-meta.{}", id, wk, format)
41        } else {
42            "output".to_string()
43        }
44    } else {
45        "output".to_string()
46    };
47
48    let output_path = target_directory.join(&file_name);
49
50    fs::create_dir_all(&target_directory).await?;
51
52    let root_attrs_obj = root_attributes.as_object().cloned().unwrap_or_default();
53    let mut inner = root_attrs_obj.clone();
54
55    if let Some(wk) = wrap_key {
56        inner.insert(wk.to_string(), content.clone());
57    } else if let Some(obj) = content.as_object() {
58        for (k, v) in obj {
59            inner.insert(k.clone(), v.clone());
60        }
61    }
62
63    let mut wrapped_inner = Map::new();
64    wrapped_inner.insert(root_element_name.to_string(), Value::Object(inner));
65
66    if let Some(decl) = xml_declaration.filter(|d| d.is_object()) {
67        let mut root = Map::new();
68        root.insert("?xml".to_string(), decl);
69        for (k, v) in wrapped_inner {
70            root.insert(k, v);
71        }
72        wrapped_inner = root;
73    }
74
75    let wrapped_xml = Value::Object(wrapped_inner);
76
77    let output_string = if let Some(s) = transform_format(format, &wrapped_xml).await {
78        s
79    } else {
80        build_xml_string(&wrapped_xml)
81    };
82
83    let mut file = fs::File::create(&output_path).await?;
84    file.write_all(output_string.as_bytes()).await?;
85    log::debug!("Created disassembled file: {}", output_path.display());
86
87    Ok(())
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use serde_json::json;
94
95    fn opts_base(disassembled_path: &str) -> BuildDisassembledFileOptions<'_> {
96        BuildDisassembledFileOptions {
97            content: json!({ "a": "b" }),
98            disassembled_path,
99            output_file_name: Some("out.xml"),
100            subdirectory: None,
101            wrap_key: None,
102            is_grouped_array: false,
103            root_element_name: "Root",
104            root_attributes: Value::Object(Map::new()),
105            xml_declaration: None,
106            format: "xml",
107            unique_id_elements: None,
108        }
109    }
110
111    #[tokio::test]
112    async fn build_disassembled_file_file_name_output_when_wrap_key_no_output_name_grouped_array() {
113        // wrap_key Some, is_grouped_array true → file_name = "output"
114        let temp = tempfile::tempdir().unwrap();
115        let path = temp.path().to_str().unwrap();
116        let mut opts = opts_base(path);
117        opts.output_file_name = None;
118        opts.wrap_key = Some("wrap");
119        opts.is_grouped_array = true;
120        opts.content = json!([{ "x": "1" }]);
121        build_disassembled_file(opts).await.unwrap();
122        assert!(temp.path().join("output").exists());
123    }
124
125    #[tokio::test]
126    async fn build_disassembled_file_file_name_output_when_wrap_key_content_not_object() {
127        // wrap_key Some, content not object (e.g. Array) → file_name = "output"
128        let temp = tempfile::tempdir().unwrap();
129        let path = temp.path().to_str().unwrap();
130        let mut opts = opts_base(path);
131        opts.output_file_name = None;
132        opts.wrap_key = Some("wrap");
133        opts.is_grouped_array = false;
134        opts.content = json!([{ "id": "a" }]);
135        build_disassembled_file(opts).await.unwrap();
136        assert!(temp.path().join("output").exists());
137    }
138
139    #[tokio::test]
140    async fn build_disassembled_file_file_name_output_when_no_wrap_key_no_output_name() {
141        // No output_file_name, no wrap_key → file_name = "output"
142        let temp = tempfile::tempdir().unwrap();
143        let path = temp.path().to_str().unwrap();
144        let mut opts = opts_base(path);
145        opts.output_file_name = None;
146        opts.wrap_key = None;
147        build_disassembled_file(opts).await.unwrap();
148        assert!(temp.path().join("output").exists());
149    }
150
151    #[tokio::test]
152    async fn build_disassembled_file_content_not_object_no_spread() {
153        // No wrap_key, content not object → inner not updated from content (only root_attributes)
154        let temp = tempfile::tempdir().unwrap();
155        let path = temp.path().to_str().unwrap();
156        let mut opts = opts_base(path);
157        opts.output_file_name = Some("single.xml");
158        opts.wrap_key = None;
159        opts.content = json!(42);
160        build_disassembled_file(opts).await.unwrap();
161        let out = fs::read_to_string(temp.path().join("single.xml"))
162            .await
163            .unwrap();
164        assert!(out.contains("<Root>"));
165        // content 42 is not spread (only objects are); root is empty
166        assert!(out.contains("</Root>"));
167    }
168}