use serde::Deserialize; #[derive(Debug, Clone, PartialEq, Eq)] pub enum TestPlanStatus { Approved, WaitingForApproval, Unknown(String), } #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct StoryMetadata { pub name: Option, pub test_plan: Option, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum StoryMetaError { MissingFrontMatter, InvalidFrontMatter(String), } #[derive(Debug, Deserialize)] struct FrontMatter { name: Option, test_plan: Option, } pub fn parse_front_matter(contents: &str) -> Result { let mut lines = contents.lines(); let first = lines.next().unwrap_or_default().trim(); if first != "---" { return Err(StoryMetaError::MissingFrontMatter); } let mut front_lines = Vec::new(); for line in &mut lines { let trimmed = line.trim(); if trimmed == "---" { let raw = front_lines.join("\n"); let front: FrontMatter = serde_yaml::from_str(&raw) .map_err(|e| StoryMetaError::InvalidFrontMatter(e.to_string()))?; return Ok(build_metadata(front)); } front_lines.push(line); } Err(StoryMetaError::InvalidFrontMatter( "Missing closing front matter delimiter".to_string(), )) } fn build_metadata(front: FrontMatter) -> StoryMetadata { let test_plan = front.test_plan.as_deref().map(parse_test_plan_status); StoryMetadata { name: front.name, test_plan, } } fn parse_test_plan_status(value: &str) -> TestPlanStatus { match value { "approved" => TestPlanStatus::Approved, "waiting_for_approval" => TestPlanStatus::WaitingForApproval, other => TestPlanStatus::Unknown(other.to_string()), } } #[cfg(test)] mod tests { use super::*; #[test] fn parses_front_matter_metadata() { let input = r#"--- name: Establish the TDD Workflow and Gates test_plan: approved workflow: tdd --- # Story 26 "#; let meta = parse_front_matter(input).expect("front matter"); assert_eq!( meta, StoryMetadata { name: Some("Establish the TDD Workflow and Gates".to_string()), test_plan: Some(TestPlanStatus::Approved), } ); } #[test] fn rejects_missing_front_matter() { let input = "# Story 26\n"; assert_eq!( parse_front_matter(input), Err(StoryMetaError::MissingFrontMatter) ); } #[test] fn rejects_unclosed_front_matter() { let input = "---\nname: Test\n"; assert!(matches!( parse_front_matter(input), Err(StoryMetaError::InvalidFrontMatter(_)) )); } }