Skip to main content

config_disassembler/
meta.rs

1//! Metadata sidecar describing a disassembled directory.
2//!
3//! A `.config-disassembler.json` file is written into the disassembly output
4//! directory so reassembly can reconstruct the original key order, root type,
5//! and the format the split files were written in.
6
7use std::fs;
8use std::path::Path;
9
10use serde::{Deserialize, Serialize};
11
12use crate::error::Result;
13use crate::format::Format;
14
15/// File name of the metadata sidecar.
16pub const META_FILENAME: &str = ".config-disassembler.json";
17
18/// Description of how a disassembled directory was produced.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Meta {
21    /// Format the original input file was read from.
22    pub source_format: SerdeFormat,
23    /// Format used to write the split files on disk.
24    pub file_format: SerdeFormat,
25    /// Original input file name (with extension), used as a default when
26    /// reassembling without an explicit output path.
27    pub source_filename: Option<String>,
28    /// Whether the document root was an object or an array.
29    pub root: Root,
30    /// Indentation string used in the original JSONC source (e.g. `"  "`,
31    /// `"    "`, `"\t"`). Present only for JSONC→JSONC disassembly.
32    /// Absent in sidecars produced before this field was added; reassembly
33    /// falls back to two spaces in that case.
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub indent: Option<String>,
36}
37
38/// Description of the document root.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40#[serde(tag = "kind", rename_all = "snake_case")]
41pub enum Root {
42    /// The root was a JSON object.
43    Object {
44        /// Original key ordering at the root.
45        key_order: Vec<String>,
46        /// Map from key to the file containing that key's value, for keys
47        /// whose value is a non-scalar (object or array). Scalars are
48        /// inlined into [`main_file`].
49        ///
50        /// [`main_file`]: Root::Object::main_file
51        key_files: std::collections::BTreeMap<String, String>,
52        /// File name (relative to the meta dir) containing all scalar
53        /// top-level keys, or `None` if there were none.
54        main_file: Option<String>,
55    },
56    /// The root was a JSON array.
57    Array {
58        /// File names (relative to the meta dir) for each array element,
59        /// in original order.
60        files: Vec<String>,
61    },
62}
63
64/// Serde-friendly metadata format type.
65///
66/// This remains as an alias for compatibility with callers that referenced
67/// `meta::SerdeFormat`; the actual serde behavior now lives on [`Format`].
68pub type SerdeFormat = Format;
69
70impl Meta {
71    /// Write the metadata file into `dir`.
72    pub fn write(&self, dir: &Path) -> Result<()> {
73        let path = dir.join(META_FILENAME);
74        let text = serde_json::to_string_pretty(self)?;
75        fs::write(path, text)?;
76        Ok(())
77    }
78
79    /// Read the metadata file from `dir`.
80    pub fn read(dir: &Path) -> Result<Self> {
81        let path = dir.join(META_FILENAME);
82        let text = fs::read_to_string(&path).map_err(|e| {
83            crate::error::Error::Invalid(format!(
84                "could not read metadata file {}: {e}",
85                path.display()
86            ))
87        })?;
88        Ok(serde_json::from_str(&text)?)
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn serde_format_round_trip() {
98        for &fmt in Format::ALL {
99            let text = serde_json::to_string(&fmt).unwrap();
100            let back: SerdeFormat = serde_json::from_str(&text).unwrap();
101            assert_eq!(fmt, back);
102        }
103    }
104
105    #[test]
106    fn read_returns_error_on_malformed_json() {
107        let tmp = tempfile::tempdir().unwrap();
108        std::fs::write(tmp.path().join(META_FILENAME), "{ not valid json }").unwrap();
109        let err = Meta::read(tmp.path()).unwrap_err();
110        // serde_json parse failure propagates as Error::Json
111        assert!(err.to_string().contains("json"), "got: {err}");
112    }
113
114    #[test]
115    fn read_returns_invalid_when_missing() {
116        let tmp = tempfile::tempdir().unwrap();
117        let err = Meta::read(tmp.path()).unwrap_err();
118        assert!(err.to_string().contains("metadata"));
119    }
120
121    #[test]
122    fn write_and_read_round_trip_object_root() {
123        let tmp = tempfile::tempdir().unwrap();
124        let meta = Meta {
125            source_format: SerdeFormat::Json,
126            file_format: SerdeFormat::Yaml,
127            source_filename: Some("orig.json".into()),
128            root: Root::Object {
129                key_order: vec!["a".into(), "b".into()],
130                key_files: std::collections::BTreeMap::new(),
131                main_file: Some("_main.yaml".into()),
132            },
133            indent: None,
134        };
135        meta.write(tmp.path()).unwrap();
136        let back = Meta::read(tmp.path()).unwrap();
137        assert!(matches!(back.root, Root::Object { .. }));
138    }
139
140    #[test]
141    fn write_and_read_round_trip_array_root() {
142        let tmp = tempfile::tempdir().unwrap();
143        let meta = Meta {
144            source_format: SerdeFormat::Yaml,
145            file_format: SerdeFormat::Json5,
146            source_filename: None,
147            root: Root::Array {
148                files: vec!["1.json5".into(), "2.json5".into()],
149            },
150            indent: None,
151        };
152        meta.write(tmp.path()).unwrap();
153        let back = Meta::read(tmp.path()).unwrap();
154        match back.root {
155            Root::Array { files } => assert_eq!(files.len(), 2),
156            _ => panic!("expected array root"),
157        }
158    }
159}