6
\$\begingroup\$

I am learning Rust.

I also play EVE Online – a video game about internet spaceships. I decided that it would be fun to practice Rust by writing a simple utility to help me x up.

to x up (verb)

  1. (EVE Online) To respond to a fleet commander with a list of ships one has available for combat.

All fleet compositions (a.k.a doctrines) have been obfuscated to protect the operational security of my character's alliance.

For example, if a fleet commander requests:

X-up for Shield Destroyers!

I might respond with:

x Svipul (default) / Burst

This indicates that I am available to pilot either a Svipul or a Burst (two specific kinds of internet spaceship) in the upcoming engagement.

Of course, I could look into my hangar and type that out manually, but I'm learning a new programming language so I may as well build an overkill utility.

Behavior

enter image description here

Source Code

Data File

I record my doctrines in YAML (the real file is a mite longer).

# //OPSEC// ALL DOCTRINES OBFUSCATED //OPSEC//

- name: Armor Battleships
  categories:
  - category: &tempests
      [
      # Tempest,
      ]
  - category: &hic
      [
      Onyx,
      ]
  - category: &ewar
      [
      Blackbird,
      Celestis,
      Maller,
      ]
  - category: &tackle
      [
      Sabre,
      Slasher,
      ]
- name: Heavy Armor
  categories:
  - category: &myrms
     [
     Myrmidon,
     ]
  - category: *ewar
  - category: *tackle
- name: Shield Destroyers
  categories:
  - category: &svipuls
      [
      Svipul (default),
      ]
  - category: &bursts
      [
      Burst,
      ]
  - category: *tackle

# //OPSEC// ALL DOCTRINES OBFUSCATED //OPSEC//

YAML anchors let me put common ship categories into multiple doctrines. When I accidentally blow up one of my ships, I simply comment out the name so that my x-up strings reflect my inventory.

Rust CLI

doctrine.rs

This parses my YAML file into Doctrine objects.

extern crate yaml_rust;
use self::yaml_rust::Yaml;

#[derive(Debug, Clone)]
pub struct Doctrine {
    pub name: String,
    pub categories: Vec<Category>,
}

#[derive(Debug, Clone)]
pub struct Category {
    pub ships: Vec<String>,
}

impl Doctrine {
    pub fn from_yaml(yaml: Yaml) -> Result<Self, &'static str> {
        match yaml {
            Yaml::Hash(doctrine) => {
                let name = match doctrine.get(&Yaml::String("name".into())) {
                    Some(doctrine_name) => {
                        match doctrine_name.clone() {
                            Yaml::String(doctrine_name_string) => doctrine_name_string,
                            _ => return Err("Doctrine name not a string."),
                        }
                    }
                    None => return Err("Could not read doctrine name."),
                };
                let categories = match doctrine.get(&Yaml::String("categories".into())) {
                    Some(doctrine_categories) => {
                        match Category::many_from_yaml(doctrine_categories.clone()) {
                            Ok(doctrine_categories_parsed) => doctrine_categories_parsed,
                            Err(err) => return Err(err),
                        }
                    }
                    None => return Err("Doctrine has no categories."),
                };
                Ok(Doctrine {
                    name: name,
                    categories: categories,
                })
            }
            _ => Err("Expected doctrine."),
        }
    }

    pub fn many_from_yaml(doctrines: Yaml) -> Result<Vec<Self>, &'static str> {
        Ok(match doctrines {
            Yaml::Array(doctrines) => {
                try!(doctrines.into_iter().map(Doctrine::from_yaml).collect())
            }
            _ => return Err("Expected list of doctrines."),
        })
    }
}

impl Category {
    pub fn from_yaml(yaml: Yaml) -> Result<Self, &'static str> {
        match yaml {
            Yaml::Hash(category) => {
                match category.get(&Yaml::String("category".into())) {
                    Some(category_ships) => {
                        let ships = match category_ships.clone() {
                            Yaml::Array(ships) => {
                                ships.iter()
                                    .map(|ship| String::from(ship.as_str().unwrap()))
                                    .collect::<Vec<String>>()
                            }
                            _ => vec![],
                        };
                        Ok(Category { ships: ships })
                    }
                    None => Err("Could not find category."),
                }
            }
            _ => Err("Expected category."),
        }
    }

    pub fn many_from_yaml(categories: Yaml) -> Result<Vec<Self>, &'static str> {
        Ok(match categories {
            Yaml::Array(categories) => {
                try!(categories.into_iter().map(Category::from_yaml).collect())
            }
            _ => return Err("Expected list of categories."),
        })
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_parse_categories() {
        use super::yaml_rust::YamlLoader;
        let s = r#"
- category:
    [
    Blackbird,
    Celestis,
    Maller,
    ]
"#;
        let yaml = YamlLoader::load_from_str(&s).unwrap();
        match Category::many_from_yaml(yaml[0].clone()) {
            Ok(categories) => {
                assert_eq!(categories[0].ships, vec!["Blackbird", "Celestis", "Maller"]);
            } 
            Err(_) => panic!(),
        }
    }

    #[test]
    fn test_parse_empty_category() {
        use super::yaml_rust::YamlLoader;
        let s = r#"
- category:
    [
    # empty
    ]
- category:
    [Blackbird]
"#;
        let yaml = YamlLoader::load_from_str(&s).unwrap();
        match Category::many_from_yaml(yaml[0].clone()) {
            Ok(categories) => {
                assert_eq!(categories[0].ships.len(), 0);
                assert_eq!(categories[1].ships, vec!["Blackbird"]);
            } 
                        Err(_) => panic!(),
        }
    }

    #[test]
    fn test_parse_doctrines() {
        use super::yaml_rust::YamlLoader;
        let s = r#"
- name: Armor Battleships
  categories:
  - category:
      [
      Blackbird,
      Celestis,
      Maller,
     ]
"#;
        let yaml = YamlLoader::load_from_str(&s).unwrap();
        match Doctrine::many_from_yaml(yaml[0].clone()) {
            Ok(doctrines) => {
                let doctrine = doctrines[0].clone();
                assert_eq!(doctrine.name, "Armor Battleships");
                assert_eq!(doctrine.categories[0].ships,
                           vec!["Blackbird", "Celestis", "Maller"]);
            }
            Err(_) => panic!(),
        }
    }

    #[test]
    fn test_parse_doctrines_with_anchors() {
        use super::yaml_rust::YamlLoader;
        let s = r#"
- name: Armor Battleships
  categories:
  - category: &ewar
      [
      Blackbird,
      Celestis,
      Maller,
      ]
- name: Armor Confessors
  categories:
  - category: *ewar
"#;
        let yaml = YamlLoader::load_from_str(&s).unwrap(); 
        match Doctrine::many_from_yaml(yaml[0].clone()) {
            Ok(doctrines) => {
                let battleships = doctrines[0].clone();
                assert_eq!(battleships.name, "Armor Battleships");
                assert_eq!(
                    battleships.categories[0].ships,
                    vec!["Blackbird", "Celestis", "Maller"]);
                let confessors = doctrines[1].clone();
                assert_eq!(confessors.name, "Armor Confessors");
                assert_eq!(
                    confessors.categories[0].ships,
                    vec!["Blackbird", "Celestis", "Maller"]);
            },
            Err(_) => panic!(),
        } 
    } 
}

main.rs

This executable exposes an ls command to retrieve the list of doctrines, and a -d DOCTRINE flag to return the x-up string.

extern crate clap;
extern crate yaml_rust;

use clap::{App, Arg, SubCommand};
use self::yaml_rust::{Yaml, YamlLoader};
use std::borrow::ToOwned;
use std::collections::HashMap;
use std::env::home_dir;
use std::fs::File;
use std::io::Read;
use std::process::exit;

mod doctrine;

fn yaml_cfg_path() -> String {
    let yaml_cfg_relative_path = "Dropbox/Log/Xup/xup/src/xup.yaml";
    let mut home_dir = match home_dir() {
        Some(home_dir) => home_dir,
        None => panic!("Could not resolve home directory."),
    };
    home_dir.push(yaml_cfg_relative_path);
    match home_dir.to_str().map(ToOwned::to_owned) {
        Some(home_dir) => home_dir,
        None => panic!("Could not resolve YAML path."),
    }
}

fn file_contents(path: &str) -> String {
    let mut file = match File::open(&path) {
        Ok(file) => file,
        Err(err) => panic!(err),
    };
    let mut contents = String::new();
    match file.read_to_string(&mut contents) {
        Ok(_) => contents,
        Err(err) => panic!(err),
    }
}

fn load_yaml_cfg() -> Yaml {
    let yaml_cfg_path = yaml_cfg_path();
    let file_contents = file_contents(&yaml_cfg_path);
    match YamlLoader::load_from_str(&file_contents) {
        Ok(contents) => {
            let num_documents = contents.len();
            if num_documents > 1 {
                println!("{} documents found in {} ...", num_documents, yaml_cfg_path);
                println!("Reading doctrines from the first document and ignoring the rest.");
            }
            contents[0].clone()
        } 
        Err(err) => panic!(err),
    }
} 

type DoctrineName = String;

fn load_doctrines() -> HashMap<DoctrineName, doctrine::Doctrine> {
    match doctrine::Doctrine::many_from_yaml(load_yaml_cfg()) {
        Ok(doctrines) =>
            doctrines.into_iter().map(|d| (d.name.clone(), d)).collect::<HashMap<_, _>>(),
        Err(err) => panic!(err)
    }
}

fn ships(doctrine: &doctrine::Doctrine) -> Vec<String> {
    doctrine.categories.iter().flat_map(|c| c.ships.iter().cloned()).collect()
}

fn xup(doctrine: &doctrine::Doctrine) -> String {
    "x ".to_string() + &ships(doctrine).join(" / ")
}

fn main() {
    let matches = App::new("xup")
        .about("Outputs x-up string for given doctrine")
        .arg(Arg::with_name("doctrine")
            .short("d")
            .long("doctrine")
            .value_name("DOCTRINE")
            .takes_value(true))
        .subcommand(SubCommand::with_name("ls").about("Lists available doctrines"))
        .get_matches(); 

    let doctrines = load_doctrines();

    if let Some(_) = matches.subcommand_matches("ls") {
        for doctrine_name in doctrines.keys() {
            println!("{}", doctrine_name);
        }
        exit(0);
    }

    match matches.value_of("doctrine") {
        Some(doctrine_name) => {
            match doctrines.get(&String::from(doctrine_name)) {
                Some(doctrine) => println!("{}", xup(&doctrine)),
                None => println!("Requested doctrine {} not found.", doctrine_name),
            }
        }
        None => println!("No doctrine requested."),
    }
}

Cargo.toml

[package]
name = "xup"
version = "0.1.0"
authors = ["Matthew Piziak <[email protected]>"]

[dependencies]
clap = "2"

[dependencies.yaml-rust]
git = "https://github.com/chyh1990/yaml-rust.git"

Emacs bindings

(setq shell-command-switch "-c")
(setq xup-default-directory "~/Dropbox/Log/Xup/xup/src")

(defun xup-ls ()
  (let ((default-directory xup-default-directory))
    (s-lines (shell-command-to-string "cargo run ls -q"))))

(defun xup-get (doctrine)
  (let ((default-directory xup-default-directory))
    (let ((xup-string (s-chomp (shell-command-to-string (concat "cargo run -q -- -d \"" doctrine "\"")))))
      (message (concat "Doctrine copied to clipboard: " xup-string))
      (kill-new xup-string))))

(setq xup-helm-source
      '((name . "Xup")
        (candidates . xup-ls)
        (action . xup-get)))

(defcom xup
  (helm :sources '(xup-helm-source)))

I am new to Rust and it's been a while since I wrote any C code, so in particular I would like to get better at stack/heap usage and avoid .clone() where it's wasteful. Thanks in advance for the review!

\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

main.rs

  1. Filesystem paths should be represented using &Path or PathBuf. Paths are not just strings and don't even have to be UTF-8.

  2. Become familiar with the methods on Option and Result; there's a lot of good ones to be able to use. For example, this is Option::expect, and Result::expect is similar:

    match foo {
        Some(x) => x,
        None => panic!("a message"),
    }
    
  3. There's an unneeded turbofish (::<>) when collecting into a HashMap.

  4. Instead of cloning the Yaml from the vector and then throwing the vector away, you can remove the item from the vector. In this case, you can use swap_remove because we don't care about the order of the rest of the vector.

  5. Unless you are returning a non-zero exit code, I'd use return to exit from main.

  6. There's an unneeded string creation to lookup in the HashMap.

extern crate clap;
extern crate yaml_rust;

use std::collections::HashMap;
use std::env::home_dir;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};

use clap::{App, Arg, SubCommand};

use self::yaml_rust::{Yaml, YamlLoader};

mod doctrine;

fn yaml_cfg_path() -> PathBuf {
    let yaml_cfg_relative_path = "Dropbox/Log/Xup/xup/src/xup.yaml";
    let mut home_dir = home_dir().expect("Could not resolve home directory.");
    home_dir.push(yaml_cfg_relative_path);
    home_dir
}

fn file_contents(path: &Path) -> String {
    let mut file = File::open(&path).expect("Can't open file");
    let mut contents = String::new();
    file.read_to_string(&mut contents).expect("Can't read file");
    contents
}

fn load_yaml_cfg() -> Yaml {
    let yaml_cfg_path = yaml_cfg_path();
    let file_contents = file_contents(&yaml_cfg_path);
    let mut contents = YamlLoader::load_from_str(&file_contents).expect("Couldn't load");

    let num_documents = contents.len();
    if num_documents > 1 {
        println!("{} documents found in {} ...", num_documents, yaml_cfg_path.display());
        println!("Reading doctrines from the first document and ignoring the rest.");
    }
    contents.swap_remove(0)
}

type DoctrineName = String;

fn load_doctrines() -> HashMap<DoctrineName, doctrine::Doctrine> {
    let doctrines = doctrine::Doctrine::many_from_yaml(&load_yaml_cfg()).expect("Something");
    doctrines.into_iter().map(|d| (d.name.clone(), d)).collect()
}

fn ships(doctrine: &doctrine::Doctrine) -> Vec<String> {
    doctrine.categories.iter().flat_map(|c| c.ships.iter().cloned()).collect()
}

fn xup(doctrine: &doctrine::Doctrine) -> String {
    "x ".to_string() + &ships(doctrine).join(" / ")
}

fn main() {
    let matches = App::new("xup")
        .about("Outputs x-up string for given doctrine")
        .arg(Arg::with_name("doctrine")
            .short("d")
            .long("doctrine")
            .value_name("DOCTRINE")
            .takes_value(true))
        .subcommand(SubCommand::with_name("ls").about("Lists available doctrines"))
        .get_matches();

    let doctrines = load_doctrines();

    if let Some(_) = matches.subcommand_matches("ls") {
        for doctrine_name in doctrines.keys() {
            println!("{}", doctrine_name);
        }
        return;
    }

    match matches.value_of("doctrine") {
        Some(doctrine_name) => {
            match doctrines.get(doctrine_name) {
                Some(doctrine) => println!("{}", xup(&doctrine)),
                None => println!("Requested doctrine {} not found.", doctrine_name),
            }
        }
        None => println!("No doctrine requested."),
    }
}

doctrine.rs

  1. Usually, extern crate declarations are placed in the main.rs or lib.rs file. This avoids the need for the self qualifier.

  2. My first overall thought is that there is a lot of rightward-drift. I'd extract a bunch of functions to help combat this. You also get the benefit of more places to add names.

  3. The YAML library provides as_* methods that return an Option for when you only care about one type. This is quite handy with the next point.

  4. Another cause of rightward-drift is the heavy use of the match statement, usually with one or two match arms. I'd advocate using a combination of the above as_* methods with the ok_or and and_then methods to have a more chained error handling.

  5. Make a custom Result type alias. This helps ensure a consistent error type across your functions, allows you to change the error type more easily, and reduces the amount of typing you need to do.

  6. There are many unneeded pub qualifiers. Don't expose more than you need to.

  7. Generally, you can match on the dereferenced value of a type (match *foo) instead of cloning it. This is often used with the ref keyword (Some(ref value)). You can then clone the inner reference, instead of the entire matched object.

  8. Prefer to use expect over unwrap. Expect allows you to have text in there that helps you find the offending line and maybe even suggests how to fix the issue.

  9. Including one buried panic in code that generally returns Result is a timebomb. Especially in this case as the error is very possible as it's based on invalid user input.

  10. Implement PartialEq on your custom types to make tests easier to read and write.

  11. Don't forget that your tests are code too. Create helper functions to DRY them up. Don't just copy your previous test and change it a bit without looking it over.

extern crate yaml_rust;

use self::yaml_rust::{yaml, Yaml};

#[derive(Debug, Clone, PartialEq)]
pub struct Doctrine {
    pub name: String,
    pub categories: Vec<Category>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct Category {
    pub ships: Vec<String>,
}

pub type Result<T> = ::std::result::Result<T, &'static str>; //' just fixing the CR pretty-printer

fn load_name(doctrine: &yaml::Hash) -> Result<&str> {
    let name_key = Yaml::String("name".into());
    doctrine.get(&name_key)
        .ok_or("Doctrine name not a string.")
        .and_then(|name| {
            name.as_str().ok_or("Doctrine name not a string.")
        })
}

fn load_categories(doctrine: &yaml::Hash) -> Result<Vec<Category>> {
    let categories_key = Yaml::String("categories".into());
    doctrine.get(&categories_key)
        .ok_or("Doctrine has no categories.")
        .and_then(Category::many_from_yaml)
}

impl Doctrine {
    fn from_yaml(yaml: &Yaml) -> Result<Self> {
        yaml.as_hash()
            .ok_or("Expected doctrine.")
            .and_then(|doctrine| {
                let name = try!(load_name(&doctrine));
                let categories = try!(load_categories(&doctrine));

                Ok(Doctrine {
                    name: name.to_string(),
                    categories: categories,
                })
            })
    }

    pub fn many_from_yaml(doctrines: &Yaml) -> Result<Vec<Self>> {
        doctrines.as_vec()
            .ok_or("Expected list of doctrines.")
            .and_then(|doctrines| {
                doctrines.iter().map(Doctrine::from_yaml).collect()
            })
    }
}

fn load_ships(category_ships: &Yaml) -> Vec<String> {
    category_ships.as_vec()
        .map(|ships| {
            ships.iter().map(|ship| {
                let name = ship.as_str().expect("Ship name was not a string");
                String::from(name)
            }).collect()
        })
        .unwrap_or_else(Vec::new)
}

fn load_category(category: &yaml::Hash) -> Result<Category> {
    let category_key = Yaml::String("category".into());
    category.get(&category_key)
        .ok_or("Could not find category.")
        .map(|category_ships| {
            let ships = load_ships(&category_ships);
            Category { ships: ships }
        })
}

impl Category {
    fn from_yaml(yaml: &Yaml) -> Result<Self> {
        yaml.as_hash()
            .ok_or("Expected category.")
            .and_then(load_category)
    }

    fn many_from_yaml(categories: &Yaml) -> Result<Vec<Self>> {
        categories.as_vec()
            .ok_or("Expected list of categories.")
            .and_then(|categories| {
                categories.iter().map(Category::from_yaml).collect()
            })
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use super::yaml_rust::{Yaml, YamlLoader};

    fn quick_parse(s: &str) -> Yaml {
        YamlLoader::load_from_str(&s).expect("Couldn't parse test YAML").swap_remove(0)
    }

    #[test]
    fn test_parse_categories() {
        let yaml = quick_parse(r#"
- category:
    [
    Blackbird,
    Celestis,
    Maller,
    ]
"#);

        let res = Category::many_from_yaml(&yaml);
        assert_eq!(res, Ok(vec![
            Category { ships: vec!["Blackbird".into(), "Celestis".into(), "Maller".into()] },
        ]));
    }

    #[test]
    fn test_parse_empty_category() {
        let yaml = quick_parse(r#"
- category:
    [
    # empty
    ]
- category:
    [Blackbird]
"#);

        let res = Category::many_from_yaml(&yaml);
        assert_eq!(res, Ok(vec![
            Category { ships: vec![]},
            Category { ships: vec!["Blackbird".into()] },
        ]));
    }

    #[test]
    fn test_parse_doctrines() {
        let yaml = quick_parse(r#"
- name: Armor Battleships
  categories:
  - category:
      [
      Blackbird,
      Celestis,
      Maller,
     ]
"#);


        let res = Doctrine::many_from_yaml(&yaml);
        assert_eq!(res, Ok(vec![
            Doctrine { name: "Armor Battleships".into(),
                       categories: vec![
                           Category { ships: vec!["Blackbird".into(),
                                                  "Celestis".into(),
                                                  "Maller".into()] },
                       ] },
        ]));
    }

    #[test]
    fn test_parse_doctrines_with_anchors() {
        let yaml = quick_parse(r#"
- name: Armor Battleships
  categories:
  - category: &ewar
      [
      Blackbird,
      Celestis,
      Maller,
      ]
- name: Armor Confessors
  categories:
  - category: *ewar
"#);

        let res = Doctrine::many_from_yaml(&yaml);
        assert_eq!(res, Ok(vec![
            Doctrine { name: "Armor Battleships".into(),
                       categories: vec![
                           Category { ships: vec!["Blackbird".into(),
                                                  "Celestis".into(),
                                                  "Maller".into()] },
                       ] },
            Doctrine { name: "Armor Confessors".into(),
                       categories: vec![
                           Category { ships: vec!["Blackbird".into(),
                                                  "Celestis".into(),
                                                  "Maller".into()] },
                       ] },
        ]));
    }
}

and avoid .clone() where it's wasteful

I took one particular path here and chose to avoid mutating the incoming YAML object at all. That's why all the methods take a &Yaml. There is still cloning to copy the data from the YAML into your types, but a lot of redundant cloning (like of the Yaml value) have been removed.

On deeper thought, I think it's possible that you could just blow apart the Yaml type and reuse the various pieces from it. I think you'd want to do something like:

  1. Send a PR to the YAML library adding into_* counterparts for all the as_* methods.
  2. Convert the hash lookups into removes (get(key) -> remove(key)).
  3. Convert the vector iterators into by-value iterators (iter -> into_iter).

I believe it's possible to do that now, but the into_* methods would make it much easier.

\$\endgroup\$
12
  • \$\begingroup\$ I'm blown away by the thoroughness of this review. Thank you!! \$\endgroup\$ Commented Jun 25, 2016 at 15:24
  • \$\begingroup\$ @MatthewPiziak You're welcome! You can see now why I suggested having real code on your other submission. There's just so much more meat and interesting aspects in a whole program. Of course, now your next task is to embed this Rust code into EVE (it's gotta have a Lua interpreter, right?) so you don't have to switch windows. Or maybe embed it into Excel; this is EVE we're talking about ^_^. \$\endgroup\$
    – Shepmaster
    Commented Jun 25, 2016 at 15:31
  • \$\begingroup\$ Yes indeed! This review was incredibly educational. As for embedding it in EVE...I would claim too much effort to save an alt-tab, but this program is already an overkill replacement for copy-paste. ^^ \$\endgroup\$ Commented Jun 25, 2016 at 15:37
  • \$\begingroup\$ I'm revisiting this review to prepare to create an issue and PR to yaml-rust. Based on doc.rust-lang.org/nightly/style/style/naming/conversions.html, it looks like into_* methods would consume the Yaml object. Can you expound a little bit about why this is useful? \$\endgroup\$ Commented Jul 9, 2016 at 21:31
  • 1
    \$\begingroup\$ My pull request to add into_ variants has been merged. \$\endgroup\$ Commented Sep 26, 2016 at 12:22
4
\$\begingroup\$

The conclusion to Shepmaster's excellent answer pointed out that if the yaml-rust library were augmented, it would be possible to consume the Yaml object instead of taking a reference.

In order to make this possible, I submitted a pull request that added into_* counterparts for all as_* methods. In accordance with the style guide's naming conventions for conversion methods, these are methods which consume the input variable.

With Shepmaster's help, I implemented the suggested changes, which reduced the quantity of necessary allocations.

main.rs

extern crate clap;
extern crate yaml_rust;

use std::collections::HashMap;
use std::fs::File;
use std::io::Read;
use std::path::Path;

use clap::{App, Arg};

use self::yaml_rust::{Yaml, YamlLoader};

mod doctrine;

fn file_contents(path: &Path) -> String {
    let mut file = File::open(&path).expect("Can't open file");
    let mut contents = String::new();
    file.read_to_string(&mut contents).expect("Can't read file");
    contents
}

fn load_yaml_cfg(yaml_cfg_path: &Path) -> Yaml {
    let file_contents = file_contents(&yaml_cfg_path);
    let mut contents = YamlLoader::load_from_str(&file_contents).expect("Couldn't load");

    let num_documents = contents.len();
    if num_documents > 1 {
        println!("{} documents found in {} ...",
                 num_documents,
                 yaml_cfg_path.display());
        println!("Reading doctrines from the first document and ignoring the rest.");
    }
    contents.swap_remove(0)
}

type DoctrineName = String;

fn load_doctrines(yaml_cfg_path: &Path) -> HashMap<DoctrineName, doctrine::Doctrine> {
    let yaml_cfg = load_yaml_cfg(&yaml_cfg_path);
    let doctrines = doctrine::Doctrine::many_from_yaml(yaml_cfg).expect("Something");
    doctrines.into_iter().map(|d| (d.name.clone(), d)).collect()
}

fn ships(doctrine: &doctrine::Doctrine) -> Vec<String> {
    doctrine.categories.iter().flat_map(|c| c.ships.iter().cloned()).collect()
}

fn xup(doctrine: &doctrine::Doctrine) -> String {
    "x ".to_string() + &ships(doctrine).join(" / ")
}

fn main() {

    let matches = App::new("xup")
        .about("Outputs x-up string for given doctrine")
        .arg(Arg::with_name("doctrine")
            .short("d")
            .long("doctrine")
            .value_name("DOCTRINE")
            .takes_value(true))
        .arg(Arg::with_name("cfg")
            .short("c")
            .long("cfg")
            .value_name("CFG")
            .takes_value(true)
            .required(true))
        .get_matches();

    let yaml_cfg_path = Path::new(matches.value_of("cfg")
        .expect("YAML configuration file not found."));

    let doctrines = load_doctrines(yaml_cfg_path);

    match matches.value_of("doctrine") {
        Some(doctrine_name) => {
            match doctrines.get(doctrine_name) {
                Some(doctrine) => println!("{}", xup(&doctrine)),
                None => println!("Requested doctrine {} not found.", doctrine_name),
            }
        }
        None => {
            for doctrine_name in doctrines.keys() {
                println!("{}", doctrine_name);
            }
            return;
        }
    }
}

doctrine.rs

extern crate yaml_rust;

use self::yaml_rust::{yaml, Yaml};

#[derive(Debug, PartialEq)]
pub struct Doctrine {
    pub name: String,
    pub categories: Vec<Category>,
}

#[derive(Debug, PartialEq)]
pub struct Category {
    pub ships: Vec<String>,
}

pub type Result<T> = ::std::result::Result<T, &'static str>; //' just fixing the CR pretty-printer

fn load_name(mut doctrine: &mut yaml::Hash) -> Result<String> {
    let name_key = Yaml::String("name".into());
    doctrine.remove(&name_key)
            .ok_or("Doctrine name not found.")
            .and_then(|name| name.into_string().ok_or("Doctrine name not a string."))
}

fn load_categories(mut doctrine: yaml::Hash) -> Result<Vec<Category>> {
    let categories_key = Yaml::String("categories".into());
    doctrine.remove(&categories_key)
            .ok_or("Doctrine has no categories.")
            .and_then(Category::many_from_yaml)
}

impl Doctrine {
    fn from_yaml(yaml: Yaml) -> Result<Self> {
        yaml.into_hash()
            .ok_or("Expected doctrine.")
            .and_then(|mut doctrine| {
                let name = try!(load_name(&mut doctrine));
                let categories = try!(load_categories(doctrine));
                Ok(Doctrine {
                    name: name.to_string(),
                    categories: categories,
                })
            })
    }

    pub fn many_from_yaml(doctrines: Yaml) -> Result<Vec<Self>> {
        doctrines.into_vec()
                 .ok_or("Expected list of doctrines.")
                 .and_then(|doctrines| doctrines.into_iter().map(Doctrine::from_yaml).collect())
    }
}

fn load_ships(category_ships: Yaml) -> Vec<String> {
    category_ships.as_vec()
                  .map(|ships| {
                      ships.iter()
                           .map(|ship| {
                               let name = ship.as_str().expect("Ship name was not a string");
                               String::from(name)
                           })
                           .collect()
                  })
                  .unwrap_or_else(Vec::new)
}

fn load_category(mut category: yaml::Hash) -> Result<Category> {
    let category_key = Yaml::String("category".into());
    category.remove(&category_key)
            .ok_or("Could not find category.")
            .map(|category_ships| {
                let ships = load_ships(category_ships);
                Category { ships: ships }
            })
}

impl Category {
    fn from_yaml(yaml: Yaml) -> Result<Self> {
        yaml.into_hash()
            .ok_or("Expected category.")
            .and_then(load_category)
    }

    fn many_from_yaml(categories: Yaml) -> Result<Vec<Self>> {
        categories.into_vec()
            .ok_or("Expected list of categories.")
            .and_then(|categories| categories.into_iter().map(Category::from_yaml).collect())
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use super::yaml_rust::{Yaml, YamlLoader};

    fn quick_parse(s: &str) -> Yaml {
        YamlLoader::load_from_str(&s).expect("Couldn't parse test YAML").swap_remove(0)
    }

    #[test]
    fn test_parse_categories() {
        let yaml = quick_parse(r#"
- category:
    [
    Blackbird,
    Celestis,
    Maller,
    ]
"#);

        let res = Category::many_from_yaml(yaml);
        assert_eq!(res,
                   Ok(vec![
            Category { ships: vec!["Blackbird".into(), "Celestis".into(), "Maller".into()] },
        ]));
    }

    #[test]
    fn test_parse_empty_category() {
        let yaml = quick_parse(r#"
- category:
    [
    # empty
    ]
- category:
    [Blackbird]
"#);

        let res = Category::many_from_yaml(yaml);
        assert_eq!(res,
                   Ok(vec![
            Category { ships: vec![]},
            Category { ships: vec!["Blackbird".into()] },
        ]));
    }

    #[test]
    fn test_parse_doctrines() {
        let yaml = quick_parse(r#"
- name: Armor Battleships
  categories:
  - category:
      [
      Blackbird,
      Celestis,
      Maller,
     ]
"#);


        let res = Doctrine::many_from_yaml(yaml);
        assert_eq!(res,
                   Ok(vec![
            Doctrine { name: "Armor Battleships".into(),
                       categories: vec![
                           Category { ships: vec!["Blackbird".into(),
                                                  "Celestis".into(),
                                                  "Maller".into()] },
                       ] },
        ]));
    }

    #[test]
    fn test_parse_doctrines_with_anchors() {
        let yaml = quick_parse(r#"
- name: Armor Battleships
  categories:
  - category: &ewar
      [
      Blackbird,
      Celestis,
      Maller,
      ]
- name: Armor Confessors
  categories:
  - category: *ewar
"#);

        let res = Doctrine::many_from_yaml(yaml);
        assert_eq!(res,
                   Ok(vec![
            Doctrine { name: "Armor Battleships".into(),
                       categories: vec![
                           Category { ships: vec!["Blackbird".into(),
                                                  "Celestis".into(),
                                                  "Maller".into()] },
                       ] },
            Doctrine { name: "Armor Confessors".into(),
                       categories: vec![
                           Category { ships: vec!["Blackbird".into(),
                                                  "Celestis".into(),
                                                  "Maller".into()] },
                       ] },
        ]));
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.