feat: project manifest with load/save/upsert
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
08b7f94e4c
commit
bd390f953d
@@ -0,0 +1,92 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct Project {
|
||||||
|
pub name: String,
|
||||||
|
pub warm_path: PathBuf,
|
||||||
|
pub hot_target_path: PathBuf,
|
||||||
|
pub last_build: Option<DateTime<Utc>>,
|
||||||
|
pub last_active: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||||
|
pub struct Manifest {
|
||||||
|
pub projects: Vec<Project>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Manifest {
|
||||||
|
pub fn load(path: &Path) -> Result<Self> {
|
||||||
|
if !path.exists() { return Ok(Self::default()); }
|
||||||
|
let s = std::fs::read_to_string(path)
|
||||||
|
.with_context(|| format!("reading manifest at {}", path.display()))?;
|
||||||
|
toml::from_str(&s).context("parsing manifest")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self, path: &Path) -> Result<()> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let s = toml::to_string_pretty(self).context("serialising manifest")?;
|
||||||
|
std::fs::write(path, s).context("writing manifest")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, name: &str) -> Option<&Project> {
|
||||||
|
self.projects.iter().find(|p| p.name == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut(&mut self, name: &str) -> Option<&mut Project> {
|
||||||
|
self.projects.iter_mut().find(|p| p.name == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upsert(&mut self, project: Project) {
|
||||||
|
if let Some(p) = self.get_mut(&project.name.clone()) {
|
||||||
|
*p = project;
|
||||||
|
} else {
|
||||||
|
self.projects.push(project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_path() -> PathBuf {
|
||||||
|
PathBuf::from("/etc/claw-store/projects.toml")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_roundtrip_manifest() {
|
||||||
|
let mut m = Manifest::default();
|
||||||
|
m.projects.push(Project {
|
||||||
|
name: "kitchen-cash".into(),
|
||||||
|
warm_path: "/slab/projects/kitchen-cash".into(),
|
||||||
|
hot_target_path: "/hot/targets/kitchen-cash".into(),
|
||||||
|
last_build: None,
|
||||||
|
last_active: None,
|
||||||
|
});
|
||||||
|
let f = NamedTempFile::new().unwrap();
|
||||||
|
m.save(f.path()).unwrap();
|
||||||
|
let loaded = Manifest::load(f.path()).unwrap();
|
||||||
|
assert_eq!(loaded.projects.len(), 1);
|
||||||
|
assert_eq!(loaded.projects[0].name, "kitchen-cash");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_project() {
|
||||||
|
let mut m = Manifest::default();
|
||||||
|
m.projects.push(Project {
|
||||||
|
name: "zeroclaw".into(),
|
||||||
|
warm_path: "/slab/projects/zeroclaw".into(),
|
||||||
|
hot_target_path: "/hot/targets/zeroclaw".into(),
|
||||||
|
last_build: None,
|
||||||
|
last_active: None,
|
||||||
|
});
|
||||||
|
assert!(m.get("zeroclaw").is_some());
|
||||||
|
assert!(m.get("nonexistent").is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user