xtalforge_modules/
workspace.rs1use std::path::{Path, PathBuf};
4
5use serde::{Deserialize, Serialize};
6use xtalforge_core::Error;
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub struct RunRecord {
11 pub id: String,
13 pub module: String,
15 pub input: String,
17 pub status: RunStatus,
19 pub outputs: Vec<String>,
21 pub data: serde_json::Value,
23 pub created: u64,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29pub enum RunStatus {
30 Pending,
32 Running,
34 Done,
36 Failed,
38}
39
40#[derive(Debug, Clone)]
42pub struct Workspace {
43 root: PathBuf,
44}
45
46impl Workspace {
47 #[cfg(not(target_arch = "wasm32"))]
53 pub fn open(root: impl Into<PathBuf>) -> Result<Self, Error> {
54 let root = root.into();
55 std::fs::create_dir_all(&root).map_err(|error| {
56 Error::InvalidInput(format!(
57 "could not create workspace {}: {error}",
58 root.display()
59 ))
60 })?;
61 Ok(Self { root })
62 }
63
64 #[must_use]
66 pub fn root(&self) -> &Path {
67 &self.root
68 }
69
70 #[must_use]
72 pub fn run_dir(&self, id: &str) -> PathBuf {
73 self.root.join("runs").join(id)
74 }
75
76 #[cfg(not(target_arch = "wasm32"))]
83 pub fn write_run(&self, record: &RunRecord) -> Result<PathBuf, Error> {
84 let directory = self.run_dir(&record.id);
85 std::fs::create_dir_all(&directory).map_err(|error| {
86 Error::InvalidInput(format!(
87 "could not create run {}: {error}",
88 directory.display()
89 ))
90 })?;
91 let path = directory.join("run.json");
92 let text = serde_json::to_string_pretty(record)
93 .map_err(|error| Error::InvalidInput(format!("could not serialize run: {error}")))?;
94 std::fs::write(&path, text).map_err(|error| {
95 Error::InvalidInput(format!("could not write {}: {error}", path.display()))
96 })?;
97 Ok(path)
98 }
99
100 #[cfg(not(target_arch = "wasm32"))]
106 pub fn read_run(&self, id: &str) -> Result<RunRecord, Error> {
107 let path = self.run_dir(id).join("run.json");
108 let text = std::fs::read_to_string(&path).map_err(|error| {
109 Error::InvalidInput(format!("could not read {}: {error}", path.display()))
110 })?;
111 serde_json::from_str(&text).map_err(|error| {
112 Error::InvalidInput(format!("invalid run record {}: {error}", path.display()))
113 })
114 }
115
116 #[cfg(not(target_arch = "wasm32"))]
123 pub fn runs(&self) -> Result<Vec<String>, Error> {
124 let directory = self.root.join("runs");
125 let entries = match std::fs::read_dir(&directory) {
126 Ok(entries) => entries,
127 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
128 Err(error) => {
129 return Err(Error::InvalidInput(format!(
130 "could not read {}: {error}",
131 directory.display()
132 )));
133 }
134 };
135 let mut ids = Vec::new();
136 for entry in entries.flatten() {
137 if entry.path().is_dir()
138 && let Some(name) = entry.file_name().to_str()
139 {
140 ids.push(name.to_owned());
141 }
142 }
143 ids.sort();
144 Ok(ids)
145 }
146}
147
148pub fn archive_records(records: &[RunRecord]) -> Result<Vec<u8>, Error> {
155 use std::io::Write;
156 let mut buffer = std::io::Cursor::new(Vec::new());
157 {
158 let mut archive = zip::ZipWriter::new(&mut buffer);
159 let options = zip::write::SimpleFileOptions::default()
160 .compression_method(zip::CompressionMethod::Deflated)
161 .unix_permissions(0o644);
162 for record in records {
163 let text = serde_json::to_vec_pretty(record).map_err(|error| {
164 Error::InvalidInput(format!("could not serialize run {}: {error}", record.id))
165 })?;
166 archive
167 .start_file(format!("runs/{}/run.json", record.id), options)
168 .map_err(archive_error)?;
169 archive.write_all(&text).map_err(archive_error)?;
170 }
171 archive.finish().map_err(archive_error)?;
172 }
173 Ok(buffer.into_inner())
174}
175
176pub fn read_archived_records(bytes: &[u8]) -> Result<Vec<RunRecord>, Error> {
182 use std::io::Read;
183 let cursor = std::io::Cursor::new(bytes);
184 let mut archive = zip::ZipArchive::new(cursor).map_err(archive_error)?;
185 let mut records: Vec<RunRecord> = Vec::new();
186 for index in 0..archive.len() {
187 let mut file = archive.by_index(index).map_err(archive_error)?;
188 let name = file.name().map_err(archive_error)?;
189 if !name.ends_with("/run.json") {
190 continue;
191 }
192 let mut text = String::new();
193 file.read_to_string(&mut text).map_err(archive_error)?;
194 records.push(
195 serde_json::from_str(&text)
196 .map_err(|error| Error::InvalidInput(format!("invalid run record: {error}")))?,
197 );
198 }
199 records.sort_by(|first, second| first.id.cmp(&second.id));
200 Ok(records)
201}
202
203fn archive_error(error: impl std::fmt::Display) -> Error {
204 Error::InvalidInput(format!("workspace archive: {error}"))
205}