Skip to main content

Rust book chapter 8.03- Text interface to add employees

https://doc.rust-lang.org/book/ch08-03-hash-maps.html

Using a hash map and vectors, create a text interface to allow a user to add employee names to a department in a company. For example, “Add Sally to Engineering” or “Add Amir to Sales.” Then let the user retrieve a list of all people in a department or all people in the company by department, sorted alphabetically.

This language, its book and the community are just straight up awesome. I had a quick read of other reviews just before posting mine, and I decided it'd be a good idea to run cargo fmt and cargo clippy before posting. +1 for tooling!

There's two drivers for me posting this- firstly, is the data structure I'm using ok?

HashMap<String, Vec<String>>

It feels a little... strange. Because a HashMap is re-sizeable, Strings are re-sizeable and Vecs are re-sizeable. Is this safe and/or efficient? Am I overthinking?

Secondly, I've attempted to handle as many error cases as I can but I wonder whether there's any that I've missed? Can't wait to get to the automated testing chapter, where I can start to maybe tackle those problems. Anyway, without further ado:

// valid commands:
// ADD name TO department, DEL name FROM department
// LIST department, LIST, QUIT
use std::{
    collections::HashMap,
    {io, io::Write},
};

#[derive(Debug)]
struct Command {
    c_type: CommandType,
    args: Option<Vec<String>>,
}

#[derive(Debug)]
enum CommandType {
    Add,
    List,
    Del,
    Quit,
}

impl CommandType {
    fn new(val: String) -> Option<CommandType> {
        match val.as_str() {
            "ADD" => Some(CommandType::Add),
            "DEL" => Some(CommandType::Del),
            "LIST" => Some(CommandType::List),
            "QUIT" => Some(CommandType::Quit),
            _ => None,
        }
    }
}

fn main() {
    let mut employer_table: HashMap<String, Vec<String>> = HashMap::new();

    employer_table.insert(
        String::from("ENGINEERING"),
        vec![
            String::from("Lewis"),
            String::from("Ellen"),
            String::from("Alan"),
        ],
    );

    employer_table.insert(
        String::from("SALES"),
        vec![
            String::from("Jane"),
            String::from("Robert"),
            String::from("Penny"),
        ],
    );

    loop {
        let cmd: String = match user_prompt() {
            Ok(val) => val,
            _ => {
                println!("Bad user input. Please try again...");
                continue;
            }
        };

        let cmd: Command = match command_parser(cmd) {
            Some(val) => val,
            None => {
                println!("Invalid command... Please type ADD, DEL, LIST or QUIT.");
                continue;
            }
        };

        // QUIT is the only command that returns false
        if !command_executor(cmd, &mut employer_table) {
            break;
        }
    }
}

fn user_prompt() -> io::Result<String> {
    let mut buf = String::new();
    let stdin = io::stdin();

    // This is a little prompt to indicate that a terminal row is typable...
    // Copied this guide on SO to get this prompt working:
    // https://stackoverflow.com/a/41387232
    print!("> ");
    io::stdout().flush().unwrap();
    stdin.read_line(&mut buf)?;

    Ok(String::from(buf.trim())) // Remove trailing CRLF, convert str to String
}

// valid commands:
// ADD name TO department, DEL name FROM department
// LIST department, LIST, QUIT
fn command_parser(c: String) -> Option<Command> {
    let args: Vec<&str> = c.split(' ').collect(); // Convert string to array of words
    let arg0 = args[0].to_uppercase();

    match CommandType::new(arg0) {
        Some(c_type) => {
            let mut args = slice_vec_to_string_vec(args);

            if !args.is_empty()  {
                let len = args.len() - 1;
                args[len] = args[len].to_uppercase();
                Some(Command {c_type, args: Some(args) })
            } else {
                Some(Command { c_type, args: None })
            }
        }
        None => None,
    }
}

fn slice_vec_to_string_vec(s: Vec<&str>) -> Vec<String> {
    let mut tmp: Vec<String> = Vec::new();

    for i in s[1..].iter() {
        tmp.push(String::from(*i));
    }

    tmp
}

fn command_executor(cmd: Command, table: &mut HashMap<String, Vec<String>>) -> bool {
    match cmd.c_type {
        CommandType::Add => {
            match cmd.args {
                Some(arg) => {
                    add_employee(table, String::from(&arg[0][..]), String::from(&arg[2][..]))
                }
                None => println!("Invalid arguments..."),
            }
            true
        }
        CommandType::List => {
            match cmd.args {
                Some(arg) => list_all_employees(table, Some(String::from(&arg[0]))),
                None => list_all_employees(table, None),
            }
            true
        }
        CommandType::Del => {
            match cmd.args {
                Some(arg) => {
                    del_employee(table, String::from(&arg[0][..]), String::from(&arg[2][..]))
                }
                None => println!("Invalid arguments..."),
            }
            true
        }
        CommandType::Quit => {
            println!("Thank you for using this shitty system. Goodbye!");
            false
        }
    }
}

fn list_all_employees(table: &HashMap<String, Vec<String>>, opts: Option<String>) {
    match opts {
        Some(opt) => match table.get(&opt) {
            Some(values) => println!("\n{}: {:#?}", opt, values),
            None => println!("{} is an invalid department.", opt),
        },
        None => println!("{:#?}", table),
    }
}

fn add_employee(table: &mut HashMap<String, Vec<String>>, emp: String, dep: String) {
    match table.get_mut(&dep) {
        Some(ptr) => ptr.push(emp),
        None => println!(
            "Failed to get a pointer to the department: {} using employee: {}",
            dep, emp
        ),
    };
}

fn del_employee(table: &mut HashMap<String, Vec<String>>, emp: String, dep: String) {
    match table.get_mut(&dep) {
        Some(d_ptr) => match d_ptr.iter().position(|e| *e == emp) {
            Some(e_ptr) => {
                d_ptr.remove(e_ptr); // Has to have semi colon so that the arm doesn't return anything
            }
            None => println!("Unable to remove that element..."),
        },
        None => println!(
            "Failed to get a pointer to department: {} using employee: {}",
            dep, emp
        ),
    };
}
```