summaryrefslogtreecommitdiff
path: root/src/parser.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/parser.rs')
-rw-r--r--src/parser.rs196
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"]);
+ }
+}