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