160 lines
4.1 KiB
Rust
160 lines
4.1 KiB
Rust
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<String>,
|
|
pub test_plan: Option<TestPlanStatus>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum StoryMetaError {
|
|
MissingFrontMatter,
|
|
InvalidFrontMatter(String),
|
|
}
|
|
|
|
impl std::fmt::Display for StoryMetaError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
StoryMetaError::MissingFrontMatter => write!(f, "Missing front matter"),
|
|
StoryMetaError::InvalidFrontMatter(msg) => write!(f, "Invalid front matter: {msg}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct FrontMatter {
|
|
name: Option<String>,
|
|
test_plan: Option<String>,
|
|
}
|
|
|
|
pub fn parse_front_matter(contents: &str) -> Result<StoryMetadata, StoryMetaError> {
|
|
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,
|
|
}
|
|
}
|
|
|
|
pub fn parse_unchecked_todos(contents: &str) -> Vec<String> {
|
|
contents
|
|
.lines()
|
|
.filter_map(|line| {
|
|
let trimmed = line.trim();
|
|
trimmed
|
|
.strip_prefix("- [ ] ")
|
|
.map(|text| text.to_string())
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
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(_))
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_unchecked_todos_mixed() {
|
|
let input = "## AC\n- [ ] First thing\n- [x] Done thing\n- [ ] Second thing\n";
|
|
assert_eq!(
|
|
parse_unchecked_todos(input),
|
|
vec!["First thing", "Second thing"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_unchecked_todos_all_checked() {
|
|
let input = "- [x] Done\n- [x] Also done\n";
|
|
assert!(parse_unchecked_todos(input).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_unchecked_todos_no_checkboxes() {
|
|
let input = "# Story\nSome text\n- A bullet\n";
|
|
assert!(parse_unchecked_todos(input).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_unchecked_todos_leading_whitespace() {
|
|
let input = " - [ ] Indented item\n";
|
|
assert_eq!(parse_unchecked_todos(input), vec!["Indented item"]);
|
|
}
|
|
}
|