battleship game in rust

Add argument parsing

Changed files
+229 -2
src
+205
src/cli.rs
··· 1 + use std::{ 2 + error::Error, 3 + fmt::Display, 4 + net::{AddrParseError, SocketAddr}, 5 + path::PathBuf, 6 + }; 7 + 8 + #[derive(Debug, PartialEq, Eq)] 9 + pub enum Args { 10 + Help, 11 + Host { boats: PathBuf, addr: SocketAddr }, 12 + Join { boats: PathBuf, addr: SocketAddr }, 13 + } 14 + 15 + #[derive(Debug, PartialEq, Eq)] 16 + pub enum ArgsParseError { 17 + NotEnoughArguments, 18 + InvalidMode(String), 19 + InvalidAddr(AddrParseError), 20 + } 21 + 22 + impl Error for ArgsParseError { 23 + fn source(&self) -> Option<&(dyn Error + 'static)> { 24 + match self { 25 + ArgsParseError::NotEnoughArguments => None, 26 + ArgsParseError::InvalidMode(_) => None, 27 + ArgsParseError::InvalidAddr(e) => Some(e), 28 + } 29 + } 30 + } 31 + 32 + impl Display for ArgsParseError { 33 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 34 + match self { 35 + ArgsParseError::NotEnoughArguments => write!(f, "not enough arguments"), 36 + ArgsParseError::InvalidMode(mode) => write!(f, "invalid mode: '{mode}'"), 37 + ArgsParseError::InvalidAddr(e) => write!(f, "error parsing address: {e}"), 38 + } 39 + } 40 + } 41 + 42 + impl Args { 43 + pub fn parse<S: AsRef<str>>(iter: impl IntoIterator<Item = S>) -> Result<Self, ArgsParseError> { 44 + let mut iter = iter.into_iter(); 45 + let Some(arg) = iter.next() else { 46 + return Ok(Self::Help); 47 + }; 48 + 49 + if matches!(arg.as_ref(), "-h" | "--help") { 50 + return Ok(Self::Help); 51 + } 52 + 53 + let boats = PathBuf::from(arg.as_ref()); 54 + let arg = iter.next().ok_or(ArgsParseError::NotEnoughArguments)?; 55 + 56 + let addr = iter 57 + .next() 58 + .ok_or(ArgsParseError::NotEnoughArguments)? 59 + .as_ref() 60 + .parse::<SocketAddr>() 61 + .map_err(ArgsParseError::InvalidAddr)?; 62 + 63 + match arg.as_ref() { 64 + "host" => Ok(Self::Host { boats, addr }), 65 + "join" => Ok(Self::Join { boats, addr }), 66 + a => Err(ArgsParseError::InvalidMode(a.to_string())), 67 + } 68 + } 69 + 70 + pub fn print_help(arg0: &str) { 71 + println!("Battleship game!"); 72 + println!(); 73 + println!("Usage:"); 74 + println!(" {arg0} [-h|--help]"); 75 + println!(" \tPrint help information and exit."); 76 + println!(" {arg0} <BOATS_FILE> host <ADDRESS>:<PORT>"); 77 + println!(" \tHost a game. Use 127.0.0.1 for a local"); 78 + println!(" \t game. Use 0.0.0.0 to make it joinable by"); 79 + println!(" \t other hosts. IPv6 is also supported!"); 80 + println!(" {arg0} <BOATS_FILE> join <ADDRESS>:<PORT>"); 81 + println!(" \tJoin a game. IPv4 and IPv6 are supported!"); 82 + } 83 + } 84 + 85 + #[cfg(test)] 86 + mod tests { 87 + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; 88 + 89 + use super::*; 90 + 91 + #[test] 92 + fn parse_nothing() { 93 + assert_eq!(Args::parse::<String>([]), Ok(Args::Help)); 94 + } 95 + 96 + #[test] 97 + fn parse_help() { 98 + assert_eq!(Args::parse(["-h"]), Ok(Args::Help)); 99 + assert_eq!(Args::parse(["--help"]), Ok(Args::Help)); 100 + } 101 + 102 + #[test] 103 + fn parse_host() { 104 + assert_eq!( 105 + Args::parse(["./my_boats", "host", "127.0.0.1:8080"]), 106 + Ok(Args::Host { 107 + boats: "./my_boats".into(), 108 + addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080), 109 + }) 110 + ); 111 + assert_eq!( 112 + Args::parse(["./my_boats", "host", "0.0.0.0:8080"]), 113 + Ok(Args::Host { 114 + boats: "./my_boats".into(), 115 + addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8080), 116 + }) 117 + ); 118 + assert_eq!( 119 + Args::parse(["./my_boats", "host", "[::1]:8080"]), 120 + Ok(Args::Host { 121 + boats: "./my_boats".into(), 122 + addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 8080), 123 + }) 124 + ); 125 + assert_eq!( 126 + Args::parse(["./my_boats", "host", "[::]:8080"]), 127 + Ok(Args::Host { 128 + boats: "./my_boats".into(), 129 + addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 8080), 130 + }) 131 + ); 132 + } 133 + 134 + #[test] 135 + fn parse_join() { 136 + assert_eq!( 137 + Args::parse(["./my_boats", "join", "127.0.0.1:8080"]), 138 + Ok(Args::Join { 139 + boats: "./my_boats".into(), 140 + addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080), 141 + }) 142 + ); 143 + assert_eq!( 144 + Args::parse(["./my_boats", "join", "0.0.0.0:8080"]), 145 + Ok(Args::Join { 146 + boats: "./my_boats".into(), 147 + addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8080), 148 + }) 149 + ); 150 + assert_eq!( 151 + Args::parse(["./my_boats", "join", "[::1]:8080"]), 152 + Ok(Args::Join { 153 + boats: "./my_boats".into(), 154 + addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 8080), 155 + }) 156 + ); 157 + assert_eq!( 158 + Args::parse(["./my_boats", "join", "[::]:8080"]), 159 + Ok(Args::Join { 160 + boats: "./my_boats".into(), 161 + addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 8080), 162 + }) 163 + ); 164 + } 165 + 166 + #[test] 167 + fn parse_errors() { 168 + assert_eq!( 169 + Args::parse(["abcoabcuobacwa"]), 170 + Err(ArgsParseError::NotEnoughArguments) 171 + ); 172 + assert_eq!( 173 + Args::parse(["./my_boats", "foobar"]), 174 + Err(ArgsParseError::NotEnoughArguments) 175 + ); 176 + assert_eq!( 177 + Args::parse(["./my_boats", "foobar", "127.0.0.1:8080"]), 178 + Err(ArgsParseError::InvalidMode("foobar".into())) 179 + ); 180 + assert!(matches!( 181 + Args::parse(["./my_boats", "host", "blablabla"]), 182 + Err(ArgsParseError::InvalidAddr(_)) 183 + )); 184 + assert!(matches!( 185 + Args::parse(["./my_boats", "host", "localhost"]), 186 + Err(ArgsParseError::InvalidAddr(_)) 187 + )); 188 + assert!(matches!( 189 + Args::parse(["./my_boats", "host", "0.0.0.0"]), 190 + Err(ArgsParseError::InvalidAddr(_)) 191 + )); 192 + assert!(matches!( 193 + Args::parse(["./my_boats", "join", "127.0.0.1"]), 194 + Err(ArgsParseError::InvalidAddr(_)) 195 + )); 196 + assert!(matches!( 197 + Args::parse(["./my_boats", "join", "localhost"]), 198 + Err(ArgsParseError::InvalidAddr(_)) 199 + )); 200 + assert!(matches!( 201 + Args::parse(["./my_boats", "join", "localhost:8080"]), 202 + Err(ArgsParseError::InvalidAddr(_)) 203 + )); 204 + } 205 + }
+24 -2
src/main.rs
··· 1 - fn main() { 2 - println!("Hello, world!"); 1 + use std::process::ExitCode; 2 + 3 + mod cli; 4 + 5 + fn main() -> ExitCode { 6 + let mut args = std::env::args(); 7 + let arg0 = args.next(); 8 + let args = match cli::Args::parse(args) { 9 + Err(e) => { 10 + eprintln!("Invalid arguments: {e}"); 11 + return ExitCode::FAILURE; 12 + } 13 + Ok(args) => args, 14 + }; 15 + 16 + eprintln!("Parsed arguments: {args:#?}"); 17 + 18 + match args { 19 + cli::Args::Help => cli::Args::print_help(arg0.as_deref().unwrap_or("navy")), 20 + cli::Args::Host { .. } => todo!(), 21 + cli::Args::Join { .. } => todo!(), 22 + } 23 + 24 + ExitCode::SUCCESS 3 25 }