RustArena

Step 7 of 7

Modeling with enums: parsing commands

Enums and Option together let you design types where bad data simply can't exist. Two patterns come up constantly at work.

States that carry their own data

Picture a network connection modeled with a struct:

struct Connection {
    connected: bool,
    connecting: bool,
    attempt: u32,      // only meaningful while connecting
    session_id: u64,   // only meaningful while connected
}

What does connected: true, connecting: true mean? What's the session id of a disconnected connection? Every function has to remember the unwritten rules. As an enum, each state carries exactly the data that makes sense for it, and impossible combinations can't be built:

enum Connection {
    Disconnected,
    Connecting { attempt: u32 },
    Connected { session_id: u64 },
}

fn status(conn: &Connection) -> String {
    match conn {
        Connection::Disconnected => String::from("offline"),
        Connection::Connecting { attempt } => format!("connecting (attempt {})", attempt),
        Connection::Connected { session_id } => format!("online, session {}", session_id),
    }
}

fn main() {
    let states = [
        Connection::Disconnected,
        Connection::Connecting { attempt: 2 },
        Connection::Connected { session_id: 9041 },
    ];
    for s in &states {
        println!("{}", status(s));
    }
}
offline
connecting (attempt 2)
online, session 9041

Parse once, at the edge

Text from users, files and networks is messy. The robust approach is to turn it into an enum as soon as it enters the program, with a function that returns Option (or, later, Result). Everything after that works with clean, typed values: no more string comparisons scattered around, no "what if the number is missing?" deep inside the logic. People call this parse, don't validate.

enum ChatCommand {
    Join(String),
    Nick(String),
    Quit,
}

fn parse(line: &str) -> Option<ChatCommand> {
    let mut words = line.split_whitespace();
    let Some(name) = words.next() else {
        return None;
    };
    let command = match name {
        "/join" => {
            let Some(room) = words.next() else {
                return None;
            };
            ChatCommand::Join(room.to_string())
        }
        "/nick" => {
            let Some(nick) = words.next() else {
                return None;
            };
            ChatCommand::Nick(nick.to_string())
        }
        "/quit" => ChatCommand::Quit,
        _ => return None,
    };
    Some(command)
}

fn run(cmd: &ChatCommand) -> String {
    match cmd {
        ChatCommand::Join(room) => format!("joining #{}", room),
        ChatCommand::Nick(nick) => format!("you are now {}", nick),
        ChatCommand::Quit => String::from("bye"),
    }
}

fn main() {
    let lines = ["/join rust", "/nick ferris", "/dance", "/join", "/quit"];
    for line in lines {
        if let Some(cmd) = parse(line) {
            println!("{}", run(&cmd));
        } else {
            println!("can't parse: {}", line);
        }
    }
}
joining #rust
you are now ferris
can't parse: /dance
can't parse: /join
bye

Your task

Write fn parse_command(line: &str) -> Option<Command> for a warehouse robot. Valid lines are:

Anything else is None: an unknown word, a missing or invalid number, an empty line, or extra words after a complete command (left now).

Challenge 2

Same technique for a bank: write fn parse_transaction(line: &str) -> Option<Transaction>. Valid lines are:

Anything else is None, including missing or invalid numbers, a transfer without a name, and extra words.

Challenge 3

Put it all together: a robot program that reads commands from standard input. The starter has Direction (with turning methods), Command and a working parse_command. Write a Robot struct (position and facing) with methods, and a main that runs each line:

For example:

forward 3
right
forward 2
report

prints 2,3 East.

Previous: Methods on enums and matches!