use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
pub const PLAN_VERSION: u32 = 1;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Status {
#[default]
Pending,
Doing,
Done,
Skipped,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PlanItem {
pub text: String,
#[serde(default)]
pub status: Status,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Plan {
pub version: u32,
#[serde(default)]
pub items: Vec<PlanItem>,
}
impl Default for Plan {
fn default() -> Self {
Plan { version: PLAN_VERSION, items: Vec::new() }
}
}
pub fn path_for_map(map_path: &Path) -> PathBuf {
let name = map_path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
let stem = name.strip_suffix(".map.ron").or_else(|| name.strip_suffix(".ron")).unwrap_or(&name);
map_path.with_file_name(format!("{stem}.plan.ron"))
}
impl Plan {
pub fn new(items: Vec<PlanItem>) -> Result<Plan, String> {
if let Some(at) = items.iter().position(|i| i.text.trim().is_empty()) {
return Err(format!("plan: item {} has no text, and an item says what the agent will do", at + 1));
}
Ok(Plan { version: PLAN_VERSION, items })
}
pub fn from_ron(text: &str) -> Result<Plan, String> {
let plan: Plan = ron::from_str(text).map_err(|e| format!("plan: parse: {e}"))?;
if plan.version != PLAN_VERSION {
return Err(format!("plan: version {} is not the {PLAN_VERSION} this build reads", plan.version));
}
Ok(plan)
}
pub fn to_ron(&self) -> Result<String, String> {
let mut text = ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default())
.map_err(|e| format!("plan: serialize: {e}"))?;
if !text.ends_with('\n') {
text.push('\n');
}
Ok(text)
}
pub fn load(path: &Path) -> Result<Plan, String> {
match std::fs::read_to_string(path) {
Ok(text) => Plan::from_ron(&text).map_err(|e| format!("{}: {e}", path.display())),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Plan::default()),
Err(e) => Err(format!("plan: {}: {e}", path.display())),
}
}
pub fn save(&self, path: &Path) -> Result<(), String> {
if self.items.is_empty() && !path.exists() {
return Ok(());
}
crate::ron_surgery::save_atomic(path, &self.to_ron()?)
}
pub fn counts(&self) -> (usize, usize, usize, usize) {
let n = |s: Status| self.items.iter().filter(|i| i.status == s).count();
(n(Status::Pending), n(Status::Doing), n(Status::Done), n(Status::Skipped))
}
}