use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::intent::UseCase;
pub const LANE_DEFAULTS_FILE: &str = "lane-defaults.json";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LaneDefault {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project: Option<String>,
pub use_case: UseCase,
pub model_id: String,
#[serde(default)]
pub set_at: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LaneDefaults {
#[serde(default)]
pub defaults: Vec<LaneDefault>,
}
impl LaneDefaults {
pub fn resolve(&self, project: Option<&str>, use_case: UseCase) -> Option<&str> {
if let Some(p) = project {
if let Some(d) = self
.defaults
.iter()
.find(|d| d.use_case == use_case && d.project.as_deref() == Some(p))
{
return Some(&d.model_id);
}
}
self.defaults
.iter()
.find(|d| d.use_case == use_case && d.project.is_none())
.map(|d| d.model_id.as_str())
}
pub fn set(&mut self, project: Option<String>, use_case: UseCase, model_id: String, now: u64) {
self.defaults
.retain(|d| !(d.use_case == use_case && d.project == project));
self.defaults.push(LaneDefault {
project,
use_case,
model_id,
set_at: now,
});
}
pub fn clear(&mut self, project: Option<&str>, use_case: UseCase) -> bool {
let before = self.defaults.len();
self.defaults
.retain(|d| !(d.use_case == use_case && d.project.as_deref() == project));
self.defaults.len() != before
}
}
pub fn default_path() -> PathBuf {
let home = std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."));
home.join(".car").join(LANE_DEFAULTS_FILE)
}
pub fn load_from(path: &Path) -> LaneDefaults {
match std::fs::read_to_string(path) {
Ok(s) => serde_json::from_str(&s).unwrap_or_default(),
Err(_) => LaneDefaults::default(),
}
}
pub fn save_to(path: &Path, defaults: &LaneDefaults) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(defaults)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
let tmp = path.with_extension("json.tmp");
std::fs::write(&tmp, json)?;
std::fs::rename(&tmp, path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn project_overrides_global_then_falls_back() {
let mut d = LaneDefaults::default();
d.set(None, UseCase::Coding, "global-coder".into(), 1);
d.set(Some("repoA".into()), UseCase::Coding, "repoA-coder".into(), 2);
assert_eq!(d.resolve(Some("repoA"), UseCase::Coding), Some("repoA-coder"));
assert_eq!(d.resolve(Some("repoB"), UseCase::Coding), Some("global-coder"));
assert_eq!(d.resolve(None, UseCase::Coding), Some("global-coder"));
assert_eq!(d.resolve(None, UseCase::Assistant), None);
}
#[test]
fn set_replaces_and_clear_removes() {
let mut d = LaneDefaults::default();
d.set(None, UseCase::Assistant, "a1".into(), 1);
d.set(None, UseCase::Assistant, "a2".into(), 2); assert_eq!(d.defaults.len(), 1);
assert_eq!(d.resolve(None, UseCase::Assistant), Some("a2"));
assert!(d.clear(None, UseCase::Assistant));
assert!(!d.clear(None, UseCase::Assistant)); assert_eq!(d.resolve(None, UseCase::Assistant), None);
}
#[test]
fn roundtrip_to_disk() {
let dir = std::env::temp_dir().join("car-lane-defaults-test");
let _ = std::fs::remove_dir_all(&dir);
let path = dir.join(LANE_DEFAULTS_FILE);
let mut d = LaneDefaults::default();
d.set(Some("p".into()), UseCase::Coding, "m".into(), 5);
save_to(&path, &d).unwrap();
let loaded = load_from(&path);
assert_eq!(loaded.resolve(Some("p"), UseCase::Coding), Some("m"));
let _ = std::fs::remove_dir_all(&dir);
}
}