battleship game in rust

Add argument parsing

Changed files
+229 -2
src
+205
src/cli.rs
···
+
use std::{
+
error::Error,
+
fmt::Display,
+
net::{AddrParseError, SocketAddr},
+
path::PathBuf,
+
};
+
+
#[derive(Debug, PartialEq, Eq)]
+
pub enum Args {
+
Help,
+
Host { boats: PathBuf, addr: SocketAddr },
+
Join { boats: PathBuf, addr: SocketAddr },
+
}
+
+
#[derive(Debug, PartialEq, Eq)]
+
pub enum ArgsParseError {
+
NotEnoughArguments,
+
InvalidMode(String),
+
InvalidAddr(AddrParseError),
+
}
+
+
impl Error for ArgsParseError {
+
fn source(&self) -> Option<&(dyn Error + 'static)> {
+
match self {
+
ArgsParseError::NotEnoughArguments => None,
+
ArgsParseError::InvalidMode(_) => None,
+
ArgsParseError::InvalidAddr(e) => Some(e),
+
}
+
}
+
}
+
+
impl Display for ArgsParseError {
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
match self {
+
ArgsParseError::NotEnoughArguments => write!(f, "not enough arguments"),
+
ArgsParseError::InvalidMode(mode) => write!(f, "invalid mode: '{mode}'"),
+
ArgsParseError::InvalidAddr(e) => write!(f, "error parsing address: {e}"),
+
}
+
}
+
}
+
+
impl Args {
+
pub fn parse<S: AsRef<str>>(iter: impl IntoIterator<Item = S>) -> Result<Self, ArgsParseError> {
+
let mut iter = iter.into_iter();
+
let Some(arg) = iter.next() else {
+
return Ok(Self::Help);
+
};
+
+
if matches!(arg.as_ref(), "-h" | "--help") {
+
return Ok(Self::Help);
+
}
+
+
let boats = PathBuf::from(arg.as_ref());
+
let arg = iter.next().ok_or(ArgsParseError::NotEnoughArguments)?;
+
+
let addr = iter
+
.next()
+
.ok_or(ArgsParseError::NotEnoughArguments)?
+
.as_ref()
+
.parse::<SocketAddr>()
+
.map_err(ArgsParseError::InvalidAddr)?;
+
+
match arg.as_ref() {
+
"host" => Ok(Self::Host { boats, addr }),
+
"join" => Ok(Self::Join { boats, addr }),
+
a => Err(ArgsParseError::InvalidMode(a.to_string())),
+
}
+
}
+
+
pub fn print_help(arg0: &str) {
+
println!("Battleship game!");
+
println!();
+
println!("Usage:");
+
println!(" {arg0} [-h|--help]");
+
println!(" \tPrint help information and exit.");
+
println!(" {arg0} <BOATS_FILE> host <ADDRESS>:<PORT>");
+
println!(" \tHost a game. Use 127.0.0.1 for a local");
+
println!(" \t game. Use 0.0.0.0 to make it joinable by");
+
println!(" \t other hosts. IPv6 is also supported!");
+
println!(" {arg0} <BOATS_FILE> join <ADDRESS>:<PORT>");
+
println!(" \tJoin a game. IPv4 and IPv6 are supported!");
+
}
+
}
+
+
#[cfg(test)]
+
mod tests {
+
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
+
+
use super::*;
+
+
#[test]
+
fn parse_nothing() {
+
assert_eq!(Args::parse::<String>([]), Ok(Args::Help));
+
}
+
+
#[test]
+
fn parse_help() {
+
assert_eq!(Args::parse(["-h"]), Ok(Args::Help));
+
assert_eq!(Args::parse(["--help"]), Ok(Args::Help));
+
}
+
+
#[test]
+
fn parse_host() {
+
assert_eq!(
+
Args::parse(["./my_boats", "host", "127.0.0.1:8080"]),
+
Ok(Args::Host {
+
boats: "./my_boats".into(),
+
addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080),
+
})
+
);
+
assert_eq!(
+
Args::parse(["./my_boats", "host", "0.0.0.0:8080"]),
+
Ok(Args::Host {
+
boats: "./my_boats".into(),
+
addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8080),
+
})
+
);
+
assert_eq!(
+
Args::parse(["./my_boats", "host", "[::1]:8080"]),
+
Ok(Args::Host {
+
boats: "./my_boats".into(),
+
addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 8080),
+
})
+
);
+
assert_eq!(
+
Args::parse(["./my_boats", "host", "[::]:8080"]),
+
Ok(Args::Host {
+
boats: "./my_boats".into(),
+
addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 8080),
+
})
+
);
+
}
+
+
#[test]
+
fn parse_join() {
+
assert_eq!(
+
Args::parse(["./my_boats", "join", "127.0.0.1:8080"]),
+
Ok(Args::Join {
+
boats: "./my_boats".into(),
+
addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080),
+
})
+
);
+
assert_eq!(
+
Args::parse(["./my_boats", "join", "0.0.0.0:8080"]),
+
Ok(Args::Join {
+
boats: "./my_boats".into(),
+
addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8080),
+
})
+
);
+
assert_eq!(
+
Args::parse(["./my_boats", "join", "[::1]:8080"]),
+
Ok(Args::Join {
+
boats: "./my_boats".into(),
+
addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 8080),
+
})
+
);
+
assert_eq!(
+
Args::parse(["./my_boats", "join", "[::]:8080"]),
+
Ok(Args::Join {
+
boats: "./my_boats".into(),
+
addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 8080),
+
})
+
);
+
}
+
+
#[test]
+
fn parse_errors() {
+
assert_eq!(
+
Args::parse(["abcoabcuobacwa"]),
+
Err(ArgsParseError::NotEnoughArguments)
+
);
+
assert_eq!(
+
Args::parse(["./my_boats", "foobar"]),
+
Err(ArgsParseError::NotEnoughArguments)
+
);
+
assert_eq!(
+
Args::parse(["./my_boats", "foobar", "127.0.0.1:8080"]),
+
Err(ArgsParseError::InvalidMode("foobar".into()))
+
);
+
assert!(matches!(
+
Args::parse(["./my_boats", "host", "blablabla"]),
+
Err(ArgsParseError::InvalidAddr(_))
+
));
+
assert!(matches!(
+
Args::parse(["./my_boats", "host", "localhost"]),
+
Err(ArgsParseError::InvalidAddr(_))
+
));
+
assert!(matches!(
+
Args::parse(["./my_boats", "host", "0.0.0.0"]),
+
Err(ArgsParseError::InvalidAddr(_))
+
));
+
assert!(matches!(
+
Args::parse(["./my_boats", "join", "127.0.0.1"]),
+
Err(ArgsParseError::InvalidAddr(_))
+
));
+
assert!(matches!(
+
Args::parse(["./my_boats", "join", "localhost"]),
+
Err(ArgsParseError::InvalidAddr(_))
+
));
+
assert!(matches!(
+
Args::parse(["./my_boats", "join", "localhost:8080"]),
+
Err(ArgsParseError::InvalidAddr(_))
+
));
+
}
+
}
+24 -2
src/main.rs
···
-
fn main() {
-
println!("Hello, world!");
+
use std::process::ExitCode;
+
+
mod cli;
+
+
fn main() -> ExitCode {
+
let mut args = std::env::args();
+
let arg0 = args.next();
+
let args = match cli::Args::parse(args) {
+
Err(e) => {
+
eprintln!("Invalid arguments: {e}");
+
return ExitCode::FAILURE;
+
}
+
Ok(args) => args,
+
};
+
+
eprintln!("Parsed arguments: {args:#?}");
+
+
match args {
+
cli::Args::Help => cli::Args::print_help(arg0.as_deref().unwrap_or("navy")),
+
cli::Args::Host { .. } => todo!(),
+
cli::Args::Join { .. } => todo!(),
+
}
+
+
ExitCode::SUCCESS
}