Skip to main content

config_disassembler/
reassemble.rs

1//! Reassemble a directory of split files (produced by [`disassemble`])
2//! back into a single configuration file.
3//!
4//! [`disassemble`]: crate::disassemble::disassemble
5
6use std::fs;
7use std::path::{Path, PathBuf};
8
9use serde_json::{Map, Value};
10
11use crate::error::{Error, Result};
12use crate::format::Format;
13use crate::meta::{Meta, Root};
14
15/// Options controlling reassembly.
16#[derive(Debug, Clone)]
17pub struct ReassembleOptions {
18    /// Directory containing the disassembled files and metadata.
19    pub input_dir: PathBuf,
20    /// Path to write the reassembled file to. If `None`, written next to
21    /// the input directory using the original source filename (or the
22    /// directory name with the chosen format's extension).
23    pub output: Option<PathBuf>,
24    /// Format to write the reassembled file in. Defaults to the format
25    /// recorded as the original source format in the metadata.
26    pub output_format: Option<Format>,
27    /// Remove the input directory after a successful reassembly.
28    pub post_purge: bool,
29}
30
31/// Reassemble a configuration file from a disassembled directory.
32///
33/// Returns the path of the reassembled output file.
34pub fn reassemble(opts: ReassembleOptions) -> Result<PathBuf> {
35    let dir = &opts.input_dir;
36    if !dir.is_dir() {
37        return Err(Error::Invalid(format!(
38            "input is not a directory: {}",
39            dir.display()
40        )));
41    }
42    let meta = Meta::read(dir)?;
43    let file_format: Format = meta.file_format.into();
44    let output_format: Format = opts
45        .output_format
46        .unwrap_or_else(|| meta.source_format.into());
47
48    let value = match &meta.root {
49        Root::Object {
50            key_order,
51            key_files,
52            main_file,
53        } => assemble_object(dir, key_order, key_files, main_file.as_deref(), file_format)?,
54        Root::Array { files } => assemble_array(dir, files, file_format)?,
55    };
56
57    let output_path = match opts.output.clone() {
58        Some(p) => p,
59        None => default_output_path(dir, &meta, output_format)?,
60    };
61    if let Some(parent) = output_path.parent() {
62        if !parent.as_os_str().is_empty() {
63            fs::create_dir_all(parent)?;
64        }
65    }
66    fs::write(&output_path, output_format.serialize(&value)?)?;
67
68    if opts.post_purge {
69        fs::remove_dir_all(dir)?;
70    }
71    Ok(output_path)
72}
73
74fn assemble_object(
75    dir: &Path,
76    key_order: &[String],
77    key_files: &std::collections::BTreeMap<String, String>,
78    main_file: Option<&str>,
79    file_format: Format,
80) -> Result<Value> {
81    let main_object: Map<String, Value> = match main_file {
82        Some(name) => match file_format.load(&dir.join(name))? {
83            Value::Object(map) => map,
84            _ => {
85                return Err(Error::Invalid(format!(
86                    "main scalar file {name} did not contain an object"
87                )));
88            }
89        },
90        None => Map::new(),
91    };
92
93    let mut out = Map::new();
94    for key in key_order {
95        if let Some(filename) = key_files.get(key) {
96            let value = file_format.load(&dir.join(filename))?;
97            out.insert(key.clone(), value);
98        } else if let Some(value) = main_object.get(key) {
99            out.insert(key.clone(), value.clone());
100        } else {
101            return Err(Error::Invalid(format!(
102                "metadata references key `{key}` but no file or scalar found"
103            )));
104        }
105    }
106    Ok(Value::Object(out))
107}
108
109fn assemble_array(dir: &Path, files: &[String], file_format: Format) -> Result<Value> {
110    let mut items = Vec::with_capacity(files.len());
111    for name in files {
112        items.push(file_format.load(&dir.join(name))?);
113    }
114    Ok(Value::Array(items))
115}
116
117fn default_output_path(dir: &Path, meta: &Meta, output_format: Format) -> Result<PathBuf> {
118    let parent = dir.parent().unwrap_or(Path::new("."));
119    let mut name = meta
120        .source_filename
121        .clone()
122        .or_else(|| {
123            dir.file_name()
124                .and_then(|n| n.to_str())
125                .map(|s| s.to_string())
126        })
127        .ok_or_else(|| Error::Invalid("could not determine output file name".into()))?;
128    let stem = match Path::new(&name).file_stem().and_then(|s| s.to_str()) {
129        Some(s) => s.to_string(),
130        None => name.clone(),
131    };
132    name = format!("{stem}.{}", output_format.extension());
133    Ok(parent.join(name))
134}