diff options
author | Lars Wirzenius <liw@liw.fi> | 2019-09-28 10:31:50 +0300 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2019-09-28 11:36:36 +0300 |
commit | 6d4af8bd7fbfa88a7dd3820af2b557fe072a8311 (patch) | |
tree | c677aafd7fc44ffe763b2b25610bdfab0c059219 /src/parser.rs | |
parent | 03b9822809b71fe302c83df2ddc832afd58777e0 (diff) | |
download | roadmap-6d4af8bd7fbfa88a7dd3820af2b557fe072a8311.tar.gz |
Change: move Status, Step, Roadmap, and parser to separate modules
Diffstat (limited to 'src/parser.rs')
-rw-r--r-- | src/parser.rs | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..48c4c52 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,196 @@ +use serde_yaml; +use serde_yaml::Value; +use std::collections::HashMap; +use std::error::Error; + +pub use crate::Status; +pub use crate::Step; +pub use crate::Roadmap; + +/// Result type for roadmap parsing. +type ParseResult<T> = Result<T, String>; + +/// Create a new roadmap from a YAML representation. +pub fn from_yaml(yaml: &str) -> Result<Roadmap, Box<dyn Error>> { + let mut roadmap = Roadmap::new(); + let map: HashMap<String, serde_yaml::Value> = 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<Step> { + match value { + Value::Mapping(_) => { + let label = parse_label(&value); + let mut step = Step::new(name, label); + + let status = parse_status(&value)?; + 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) -> Vec<&str> { + let key_name = "depends"; + let key = Value::String(key_name.to_string()); + let mut depends: Vec<&str> = vec![]; + + if let Some(Value::Sequence(deps)) = map.get(&key) { + for depname in deps.iter() { + if let Value::String(depname) = depname { + depends.push(depname); + } + } + } + + depends +} + +// Get label string from a Mapping element, or empty string. +fn parse_label<'a>(map: &'a Value) -> &'a str { + +parse_string("label", map) +} + +// Get status string from a Mapping element. Default to unknown status. +fn parse_status<'a>(map: &'a Value) -> ParseResult<Status> { + let text = parse_string("status", map); + let status = Status::from_text(text); + if let Some(status) = status { + Ok(status) + } else { + Err(format!("unknown status: {:?}", text)) + } +} + +// Get string value from a Mapping element, or empty string. +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? + if roadmap.count_goals() != 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 = roadmap.step_names(); + 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"]); + } +} |