use serde::Deserialize;
use anyhow::{Context, bail};
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub(crate) struct Plan {
#[serde(default)]
pub phases: Vec<PlanPhase>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub(crate) struct PlanPhase {
pub id: String,
#[serde(default)]
pub name: String,
#[serde(default)]
pub objective: String,
}
impl Plan {
pub(crate) fn parse(text: &str) -> anyhow::Result<Plan> {
#[derive(Deserialize)]
struct Raw {
#[serde(default)]
phase: Vec<PlanPhase>,
}
let raw: Raw = toml::from_str(text).context("Failed to parse plan.toml")?;
let mut seen = std::collections::BTreeSet::new();
for ph in &raw.phase {
if !seen.insert(ph.id.as_str()) {
bail!("Duplicate phase id {} in plan", ph.id);
}
}
Ok(Plan { phases: raw.phase })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plan_parse_reads_ordered_phases() {
let text = r#"
schema = "doctrine.plan.overview"
version = 1
slice = "SL-004"
[[phase]]
id = "PHASE-01"
name = "First"
objective = "do a"
[[phase]]
id = "PHASE-02"
name = "Second"
"#;
let plan = Plan::parse(text).unwrap();
let ids: Vec<&str> = plan.phases.iter().map(|p| p.id.as_str()).collect();
assert_eq!(ids, vec!["PHASE-01", "PHASE-02"]);
assert_eq!(plan.phases[0].objective, "do a");
assert_eq!(plan.phases[1].objective, "");
}
#[test]
fn plan_parse_rejects_duplicate_phase_ids() {
let text = r#"
[[phase]]
id = "PHASE-01"
[[phase]]
id = "PHASE-01"
"#;
let err = Plan::parse(text).unwrap_err();
assert!(err.to_string().contains("Duplicate phase id PHASE-01"));
}
}