use serde_yaml::Value; use std::collections::HashMap; pub use crate::Roadmap; pub use crate::RoadmapError; pub use crate::RoadmapResult; pub use crate::Status; pub use crate::Step; /// Create a new roadmap from a textual YAML representation. pub fn from_yaml(yaml: &str) -> RoadmapResult { 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); } roadmap.validate()?; Ok(roadmap) } // Convert a Value into a Step, if possible. fn step_from_value(name: &str, value: &Value) -> RoadmapResult { 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(RoadmapError::StepNotMapping), } } // Get a sequence of depenencies. fn parse_depends(map: &Value) -> RoadmapResult> { let key_name = "depends"; let key = Value::String(key_name.to_string()); let mut depends: Vec<&str> = vec![]; match map.get(&key) { None => (), Some(Value::Sequence(deps)) => { for depname in deps.iter() { match depname { Value::String(depname) => depends.push(depname), _ => return Err(RoadmapError::DependsNotNames), } } } _ => return Err(RoadmapError::DependsNotNames), } 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(map: &Value) -> RoadmapResult { let text = parse_string("status", map); match Status::from_text(text) { Some(status) => Ok(status), _ => Err(RoadmapError::UnknownStatus(text.into())), } } // 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, _ => "", } } #[cfg(test)] mod tests { use super::from_yaml; #[test] fn yaml_is_empty() { if from_yaml("").is_ok() { panic!("expected a parse error"); } } #[test] fn yaml_is_list() { if from_yaml("[]").is_ok() { panic!("expected a parse error"); } } #[test] fn yaml_map_entries_not_maps() { if from_yaml("foo: []").is_ok() { panic!("expected a parse error"); } } #[test] fn yaml_unknown_dep() { if from_yaml("foo: {depends: [bar]}").is_ok() { panic!("expected a parse error"); } } #[test] fn yaml_unknown_status() { if from_yaml(r#"foo: {status: "bar"}"#).is_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 = first.dependencies().count(); assert_eq!(deps, 0); let second = roadmap.get_step("second").unwrap(); assert_eq!(second.name(), "second"); assert_eq!(second.label(), "the second step"); let deps: Vec<&str> = second.dependencies().collect(); assert_eq!(deps, vec!["first"]); } }