tfparser_core/exporter/
manifest.rs1use std::{
10 fs::{self, OpenOptions},
11 io::Write as _,
12 path::Path,
13 sync::Arc,
14};
15
16use serde::{Deserialize, Serialize};
17
18use super::ExportError;
19
20#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
26#[non_exhaustive]
27#[serde(deny_unknown_fields)]
28pub struct Manifest {
29 pub tfparser_version: String,
31 pub schema_major: u32,
33 pub schema_minor: u32,
35 pub generated_at_ms: i64,
37 pub workspace_root: String,
39 pub command_line: String,
42 pub files: Vec<ManifestFile>,
44}
45
46#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
48#[non_exhaustive]
49#[serde(deny_unknown_fields)]
50pub struct ManifestFile {
51 pub name: String,
53 pub rows: u64,
55 pub bytes: u64,
57 pub sha256: String,
59}
60
61pub fn write_manifest(
70 manifest: &Manifest,
71 final_path: &Path,
72 overwrite: bool,
73) -> Result<u64, ExportError> {
74 if final_path.exists() && !overwrite {
75 return Err(ExportError::OutputExists(Arc::from(final_path)));
76 }
77
78 let bytes = serde_json::to_vec_pretty(manifest).map_err(|source| ExportError::Manifest {
79 path: Arc::from(final_path),
80 source,
81 })?;
82 let mut partial: std::ffi::OsString = final_path.as_os_str().to_os_string();
83 partial.push(".partial");
84 let partial = std::path::PathBuf::from(partial);
85 if partial.exists() {
86 fs::remove_file(&partial).map_err(|source| ExportError::Io {
87 path: Arc::from(partial.as_path()),
88 source,
89 })?;
90 }
91 let mut file = OpenOptions::new()
92 .write(true)
93 .create_new(true)
94 .open(&partial)
95 .map_err(|source| ExportError::Io {
96 path: Arc::from(partial.as_path()),
97 source,
98 })?;
99 file.write_all(&bytes).map_err(|source| ExportError::Io {
100 path: Arc::from(partial.as_path()),
101 source,
102 })?;
103 file.sync_all().map_err(|source| ExportError::Io {
104 path: Arc::from(partial.as_path()),
105 source,
106 })?;
107 drop(file);
108
109 fs::rename(&partial, final_path).map_err(|source| ExportError::Io {
110 path: Arc::from(partial.as_path()),
111 source,
112 })?;
113 let on_disk = fs::metadata(final_path)
114 .map(|m| m.len())
115 .map_err(|source| ExportError::Io {
116 path: Arc::from(final_path),
117 source,
118 })?;
119 Ok(on_disk)
120}
121
122#[cfg(test)]
123#[allow(
124 clippy::unwrap_used,
125 clippy::expect_used,
126 clippy::panic,
127 clippy::indexing_slicing
128)]
129mod tests {
130 use super::*;
131
132 fn fake_manifest() -> Manifest {
133 Manifest {
134 tfparser_version: "0.1.0".into(),
135 schema_major: 0,
136 schema_minor: 1,
137 generated_at_ms: 1_700_000_000_000,
138 workspace_root: "/tmp/repo".into(),
139 command_line: "tfparser parse /tmp/repo".into(),
140 files: vec![ManifestFile {
141 name: "resources.parquet".into(),
142 rows: 42,
143 bytes: 1024,
144 sha256: "deadbeef".into(),
145 }],
146 }
147 }
148
149 #[test]
150 fn test_should_round_trip_manifest_via_serde() {
151 let m = fake_manifest();
152 let s = serde_json::to_string(&m).unwrap();
153 let back: Manifest = serde_json::from_str(&s).unwrap();
154 assert_eq!(m, back);
155 }
156
157 #[test]
158 fn test_should_write_manifest_and_return_byte_count() {
159 let tmp = tempfile::tempdir().unwrap();
160 let path = tmp.path().join("workspace.manifest.json");
161 let bytes = write_manifest(&fake_manifest(), &path, false).unwrap();
162 let on_disk = fs::read(&path).unwrap();
163 assert_eq!(on_disk.len() as u64, bytes);
164 let s = String::from_utf8(on_disk).unwrap();
165 assert!(s.contains("\"resources.parquet\""), "{s}");
166 }
167
168 #[test]
169 fn test_should_refuse_overwrite_without_flag() {
170 let tmp = tempfile::tempdir().unwrap();
171 let path = tmp.path().join("workspace.manifest.json");
172 fs::write(&path, b"existing").unwrap();
173 let err = write_manifest(&fake_manifest(), &path, false).unwrap_err();
174 assert!(matches!(err, ExportError::OutputExists(_)));
175 }
176
177 #[test]
178 fn test_should_overwrite_when_flag_set() {
179 let tmp = tempfile::tempdir().unwrap();
180 let path = tmp.path().join("workspace.manifest.json");
181 fs::write(&path, b"existing").unwrap();
182 let _bytes = write_manifest(&fake_manifest(), &path, true).unwrap();
183 let s = fs::read_to_string(&path).unwrap();
184 assert!(s.contains("\"resources.parquet\""), "{s}");
185 }
186}