use serde_yaml; use serde_yaml::Value; use std::collections::HashMap; use std::error::Error; pub use crate::Roadmap; pub use crate::Status; pub use crate::Step; /// Result type for roadmap parsing. type ParseResult = Result; /// Create a new roadmap from a textual YAML representation. pub fn from_yaml(yaml: &str) -> Result> { let mut roadmap = Roadmap::new(); let map: HashMap = serde_yaml::from_str(yaml)?; for (name, value) in map { let step = step_from_value(&name, &value)?; roadmap.add_step(step); } validate(&roadmap)?; Ok(roadmap) } // Convert a Value into a Step, if possible. fn step_from_value(name: &str, value: &Value) -> ParseResult { match value { Value::Mapping(_) => { let label = parse_label(&value); let status = parse_status(&value)?; let mut step = Step::new(name, label); step.set_status(status); for depname in parse_depends(&value)?.iter() { step.add_dependency(depname); } Ok(step) } _ => Err("step is not a mapping".to_string()), } } // Get a sequence of depenencies. fn parse_depends(map: &Value) -> ParseResult> { let key_name = "depends"; let key = Value::String(key_name.to_string()); let mut depends: Vec<&str> = vec![]; let need_list_of_names = format!("'depends' must be a list of step names"); match map.get(&key) { None => (), Some(Value::Sequence(deps)) => { for depname in deps.iter() { match depname { Value::String(depname) => depends.push(depname), _ => return Err(need_list_of_names), } } } _ => return Err(need_list_of_names), } Ok(depends) } // Get label string from a Mapping element, or empty string. fn parse_label(map: &Value) -> &str { parse_string("label", map) } // Get status string from a Mapping element. Default to unknown status. fn parse_status<'a>(map: &'a Value) -> ParseResult { let text = parse_string("status", map); match Status::from_text(text) { Some(status) => Ok(status), _ => Err(format!("unknown status: {:?}", text)), } } // Get string value from a Mapping field, or empty string if field // isn't there. fn parse_string<'a>(key_name: &str, map: &'a Value) -> &'a str { let key = Value::String(key_name.to_string()); match map.get(&key) { Some(Value::String(s)) => s, _ => "", } } // Validate that the parsed, constructed roadmap is valid. fn validate(roadmap: &Roadmap) -> ParseResult<()> { // Is there exactly one goal? match roadmap.count_goals() { 0 => return Err(format!("the roadmap doesn't have a goal")), 1 => (), _ => return Err(format!("must have exactly one goal for roadmap")), } // Does every dependency exist? for step in roadmap.iter() { for depname in step.dependencies() { match roadmap.get_step(depname) { None => { return Err(format!( "step {} depends on missing {}", step.name(), depname )) } Some(_) => (), } } } Ok(()) } #[cfg(test)] mod tests { use super::from_yaml; #[test] fn yaml_is_empty() { let r = from_yaml(""); match r { Ok(_) => panic!("expected a parse error"), _ => (), } } #[test] fn yaml_is_list() { let r = from_yaml("[]"); match r { Ok(_) => panic!("expected a parse error"), _ => (), } } #[test] fn yaml_map_entries_not_maps() { let r = from_yaml("foo: []"); match r { Ok(_) => panic!("expected a parse error"), _ => (), } } #[test] fn yaml_unknown_dep() { let r = from_yaml("foo: {depends: [bar]}"); match r { Ok(_) => panic!("expected a parse error"), _ => (), } } #[test] fn yaml_unknown_status() { let r = from_yaml(r#"foo: {status: "bar"}"#); match r { Ok(_) => panic!("expected a parse error"), _ => (), } } #[test] fn yaml_happy() { let roadmap = from_yaml( " first: label: the first step second: label: the second step depends: - first ", ) .unwrap(); let names: Vec<&str> = roadmap.step_names().collect(); assert_eq!(names.len(), 2); assert!(names.contains(&"first")); assert!(names.contains(&"second")); let first = roadmap.get_step("first").unwrap(); assert_eq!(first.name(), "first"); assert_eq!(first.label(), "the first step"); let deps: Vec<&String> = first.dependencies().collect(); assert_eq!(deps.len(), 0); let second = roadmap.get_step("second").unwrap(); assert_eq!(second.name(), "second"); assert_eq!(second.label(), "the second step"); let deps: Vec<&String> = second.dependencies().collect(); assert_eq!(deps, vec!["first"]); } }