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 mirror of [`Format`].
59#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
60#[serde(rename_all = "lowercase")]
61pub enum SerdeFormat {
62    Json,
63    Json5,
64    Yaml,
65    Toml,
66}
67
68impl From<Format> for SerdeFormat {
69    fn from(f: Format) -> Self {
70        match f {
71            Format::Json => SerdeFormat::Json,
72            Format::Json5 => SerdeFormat::Json5,
73            Format::Yaml => SerdeFormat::Yaml,
74            Format::Toml => SerdeFormat::Toml,
75        }
76    }
77}
78
79impl From<SerdeFormat> for Format {
80    fn from(f: SerdeFormat) -> Self {
81        match f {
82            SerdeFormat::Json => Format::Json,
83            SerdeFormat::Json5 => Format::Json5,
84            SerdeFormat::Yaml => Format::Yaml,
85            SerdeFormat::Toml => Format::Toml,
86        }
87    }
88}
89
90impl Meta {
91    /// Write the metadata file into `dir`.
92    pub fn write(&self, dir: &Path) -> Result<()> {
93        let path = dir.join(META_FILENAME);
94        let text = serde_json::to_string_pretty(self)?;
95        fs::write(path, text)?;
96        Ok(())
97    }
98
99    /// Read the metadata file from `dir`.
100    pub fn read(dir: &Path) -> Result<Self> {
101        let path = dir.join(META_FILENAME);
102        let text = fs::read_to_string(&path).map_err(|e| {
103            crate::error::Error::Invalid(format!(
104                "could not read metadata file {}: {e}",
105                path.display()
106            ))
107        })?;
108        Ok(serde_json::from_str(&text)?)
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn serde_format_round_trip() {
118        for fmt in [Format::Json, Format::Json5, Format::Yaml, Format::Toml] {
119            let s: SerdeFormat = fmt.into();
120            let back: Format = s.into();
121            assert_eq!(fmt, back);
122        }
123    }
124
125    #[test]
126    fn read_returns_invalid_when_missing() {
127        let tmp = tempfile::tempdir().unwrap();
128        let err = Meta::read(tmp.path()).unwrap_err();
129        assert!(err.to_string().contains("metadata"));
130    }
131
132    #[test]
133    fn write_and_read_round_trip_object_root() {
134        let tmp = tempfile::tempdir().unwrap();
135        let meta = Meta {
136            source_format: SerdeFormat::Json,
137            file_format: SerdeFormat::Yaml,
138            source_filename: Some("orig.json".into()),
139            root: Root::Object {
140                key_order: vec!["a".into(), "b".into()],
141                key_files: std::collections::BTreeMap::new(),
142                main_file: Some("_main.yaml".into()),
143            },
144        };
145        meta.write(tmp.path()).unwrap();
146        let back = Meta::read(tmp.path()).unwrap();
147        assert!(matches!(back.root, Root::Object { .. }));
148    }
149
150    #[test]
151    fn write_and_read_round_trip_array_root() {
152        let tmp = tempfile::tempdir().unwrap();
153        let meta = Meta {
154            source_format: SerdeFormat::Yaml,
155            file_format: SerdeFormat::Json5,
156            source_filename: None,
157            root: Root::Array {
158                files: vec!["1.json5".into(), "2.json5".into()],
159            },
160        };
161        meta.write(tmp.path()).unwrap();
162        let back = Meta::read(tmp.path()).unwrap();
163        match back.root {
164            Root::Array { files } => assert_eq!(files.len(), 2),
165            _ => panic!("expected array root"),
166        }
167    }
168}