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)
- (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
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!