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}
31
32/// Description of the document root.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(tag = "kind", rename_all = "snake_case")]
35pub enum Root {
36    /// The root was a JSON object.
37    Object {
38        /// Original key ordering at the root.
39        key_order: Vec<String>,
40        /// Map from key to the file containing that key's value, for keys
41        /// whose value is a non-scalar (object or array). Scalars are
42        /// inlined into [`main_file`].
43        ///
44        /// [`main_file`]: Root::Object::main_file
45        key_files: std::collections::BTreeMap<String, String>,
46        /// File name (relative to the meta dir) containing all scalar
47        /// top-level keys, or `None` if there were none.
48        main_file: Option<String>,
49    },
50    /// The root was a JSON array.
51    Array {
52        /// File names (relative to the meta dir) for each array element,
53        /// in original order.
54        files: Vec<String>,
55    },
56}
57
58/// Serde-friendly metadata format type.
59///
60/// This remains as an alias for compatibility with callers that referenced
61/// `meta::SerdeFormat`; the actual serde behavior now lives on [`Format`].
62pub type SerdeFormat = Format;
63
64impl Meta {
65    /// Write the metadata file into `dir`.
66    pub fn write(&self, dir: &Path) -> Result<()> {
67        let path = dir.join(META_FILENAME);
68        let text = serde_json::to_string_pretty(self)?;
69        fs::write(path, text)?;
70        Ok(())
71    }
72
73    /// Read the metadata file from `dir`.
74    pub fn read(dir: &Path) -> Result<Self> {
75        let path = dir.join(META_FILENAME);
76        let text = fs::read_to_string(&path).map_err(|e| {
77            crate::error::Error::Invalid(format!(
78                "could not read metadata file {}: {e}",
79                path.display()
80            ))
81        })?;
82        Ok(serde_json::from_str(&text)?)
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn serde_format_round_trip() {
92        for &fmt in Format::ALL {
93            let text = serde_json::to_string(&fmt).unwrap();
94            let back: SerdeFormat = serde_json::from_str(&text).unwrap();
95            assert_eq!(fmt, back);
96        }
97    }
98
99    #[test]
100    fn read_returns_invalid_when_missing() {
101        let tmp = tempfile::tempdir().unwrap();
102        let err = Meta::read(tmp.path()).unwrap_err();
103        assert!(err.to_string().contains("metadata"));
104    }
105
106    #[test]
107    fn write_and_read_round_trip_object_root() {
108        let tmp = tempfile::tempdir().unwrap();
109        let meta = Meta {
110            source_format: SerdeFormat::Json,
111            file_format: SerdeFormat::Yaml,
112            source_filename: Some("orig.json".into()),
113            root: Root::Object {
114                key_order: vec!["a".into(), "b".into()],
115                key_files: std::collections::BTreeMap::new(),
116                main_file: Some("_main.yaml".into()),
117            },
118        };
119        meta.write(tmp.path()).unwrap();
120        let back = Meta::read(tmp.path()).unwrap();
121        assert!(matches!(back.root, Root::Object { .. }));
122    }
123
124    #[test]
125    fn write_and_read_round_trip_array_root() {
126        let tmp = tempfile::tempdir().unwrap();
127        let meta = Meta {
128            source_format: SerdeFormat::Yaml,
129            file_format: SerdeFormat::Json5,
130            source_filename: None,
131            root: Root::Array {
132                files: vec!["1.json5".into(), "2.json5".into()],
133            },
134        };
135        meta.write(tmp.path()).unwrap();
136        let back = Meta::read(tmp.path()).unwrap();
137        match back.root {
138            Root::Array { files } => assert_eq!(files.len(), 2),
139            _ => panic!("expected array root"),
140        }
141    }
142}