config_disassembler/
reassemble.rs1use 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#[derive(Debug, Clone)]
17pub struct ReassembleOptions {
18 pub input_dir: PathBuf,
20 pub output: Option<PathBuf>,
24 pub output_format: Option<Format>,
27 pub post_purge: bool,
29}
30
31pub 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}