#[derive(Debug, Clone)]
pub struct JourneyTask {
pub task: String,
pub score: i32,
pub people: Vec<String>,
pub section: String,
pub section_index: usize,
}
#[derive(Debug, Default)]
pub struct JourneyDiagram {
pub title: Option<String>,
pub tasks: Vec<JourneyTask>,
pub actors: Vec<String>,
pub sections: Vec<String>,
}
pub fn parse(input: &str) -> crate::error::ParseResult<JourneyDiagram> {
let mut diag = JourneyDiagram::default();
let mut current_section = String::new();
let mut section_index: usize = 0;
let mut header_seen = false;
let mut actor_set: Vec<String> = Vec::new();
for line in input.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with("%%") {
continue;
}
if !header_seen {
if trimmed == "journey" || trimmed.starts_with("journey ") {
header_seen = true;
}
continue;
}
if let Some(rest) = trimmed.strip_prefix("title") {
let t = rest.trim();
if !t.is_empty() {
diag.title = Some(t.to_string());
}
continue;
}
if let Some(rest) = trimmed.strip_prefix("section") {
let name = rest.trim().to_string();
if !diag.sections.contains(&name) {
diag.sections.push(name.clone());
}
if current_section != name {
if !current_section.is_empty() {
section_index += 1;
}
current_section = name;
}
continue;
}
if let Some(first_colon) = trimmed.find(':') {
let label = trimmed[..first_colon].trim().to_string();
if label.is_empty() {
continue;
}
let rest_after_label = trimmed[first_colon + 1..].trim();
let (score, people) = if let Some(second_colon) = rest_after_label.find(':') {
let score_str = rest_after_label[..second_colon].trim();
let actors_str = rest_after_label[second_colon + 1..].trim();
let score: i32 = score_str.parse().unwrap_or(0);
let people: Vec<String> = actors_str
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
(score, people)
} else {
let score: i32 = rest_after_label.parse().unwrap_or(0);
(score, vec![])
};
for actor in &people {
if !actor_set.contains(actor) {
actor_set.push(actor.clone());
}
}
diag.tasks.push(JourneyTask {
task: label,
score,
people,
section: current_section.clone(),
section_index,
});
}
}
actor_set.sort();
diag.actors = actor_set;
crate::error::ParseResult::ok(diag)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_basic() {
let input = "journey\n title My working day\n section Go to work\n Make tea: 5: Me\n Go upstairs: 3: Me\n Do work: 1: Me, Cat\n section Go home\n Go downstairs: 5: Me\n Sit down: 3: Me";
let d = parse(input).diagram;
assert_eq!(d.title.as_deref(), Some("My working day"));
assert_eq!(d.sections, vec!["Go to work", "Go home"]);
assert_eq!(d.tasks.len(), 5);
assert_eq!(d.tasks[0].task, "Make tea");
assert_eq!(d.tasks[0].score, 5);
assert_eq!(d.tasks[0].people, vec!["Me"]);
assert_eq!(d.tasks[2].people, vec!["Me", "Cat"]);
assert_eq!(d.tasks[0].section, "Go to work");
assert_eq!(d.tasks[3].section, "Go home");
assert_eq!(d.tasks[3].section_index, 1);
assert_eq!(d.actors, vec!["Cat", "Me"]); }
#[test]
fn parse_no_actors() {
let input = "journey\n title Simple\n section A\n Task one: 3";
let d = parse(input).diagram;
assert_eq!(d.tasks[0].people.len(), 0);
assert_eq!(d.tasks[0].score, 3);
}
#[test]
fn parse_no_title() {
let input = "journey\n section S\n Task: 5: X";
let d = parse(input).diagram;
assert_eq!(d.title, None);
assert_eq!(d.tasks.len(), 1);
}
}