1use std::str::FromStr;
18
19use crate::geo::Location;
20use crate::order::{
21 BuildCommand, Command, ConvoyedMove, MainCommand, MoveCommand, Order, RetreatCommand,
22 SupportedOrder,
23};
24use crate::{Nation, UnitType};
25
26mod error;
27
28pub use self::error::{Error, ErrorKind};
29
30pub trait FromWords: Sized {
32 type Err;
34
35 fn from_words(w: &[&str]) -> Result<Self, Self::Err>;
37}
38
39type ParseResult<T> = Result<T, Error>;
40
41impl<L: Location + FromStr<Err = Error>, C: Command<L> + FromWords<Err = Error>> FromStr
42 for Order<L, C>
43{
44 type Err = Error;
45
46 fn from_str(s: &str) -> ParseResult<Self> {
47 let words = s.split_whitespace().collect::<Vec<_>>();
48
49 let nation = Nation::from(words[0].trim_end_matches(':'));
50 let unit_type = words[1].parse()?;
51 let location = words[2].parse()?;
52 let cmd = C::from_words(&words[3..])?;
53
54 Ok(Order {
55 nation,
56 unit_type,
57 region: location,
58 command: cmd,
59 })
60 }
61}
62
63impl<L: Location + FromStr<Err = Error>> FromWords for MainCommand<L> {
64 type Err = Error;
65
66 fn from_words(words: &[&str]) -> ParseResult<Self> {
67 match &(words[0].to_lowercase())[..] {
68 "holds" | "hold" => Ok(MainCommand::Hold),
69 "->" => Ok(MoveCommand::from_words(&words[1..])?.into()),
70 "supports" => Ok(SupportedOrder::from_words(&words[1..])?.into()),
71 "convoys" => Ok(ConvoyedMove::from_words(&words[1..])?.into()),
72 cmd => Err(Error::new(ErrorKind::UnknownCommand, cmd)),
73 }
74 }
75}
76
77impl<L: Location + FromStr<Err = Error>> FromWords for MoveCommand<L> {
78 type Err = Error;
79
80 fn from_words(w: &[&str]) -> ParseResult<Self> {
81 const CONVOY_CASINGS: [&str; 2] = ["convoy", "Convoy"];
82
83 match w.len() {
84 1 => Ok(MoveCommand::new(w[0].parse()?)),
85 3 if w[1] == "via" && CONVOY_CASINGS.contains(&w[2]) => {
86 Ok(MoveCommand::with_mandatory_convoy(w[0].parse()?))
87 }
88 _ => Err(Error::new(ErrorKind::MalformedMove, w.join(" "))),
89 }
90 }
91}
92
93impl<L: Location + FromStr<Err = Error>> FromWords for SupportedOrder<L> {
94 type Err = Error;
95
96 fn from_words(w: &[&str]) -> ParseResult<Self> {
97 match w.len() {
98 2 => Ok(SupportedOrder::Hold(w[0].parse()?, w[1].parse()?)),
100 4 => Ok(SupportedOrder::Move(
102 w[0].parse()?,
103 w[1].parse()?,
104 w[3].parse()?,
105 )),
106 _ => Err(Error::new(ErrorKind::MalformedSupport, w.join(" "))),
107 }
108 }
109}
110
111impl<L: Location + FromStr<Err = Error>> FromWords for ConvoyedMove<L> {
112 type Err = Error;
113
114 fn from_words(w: &[&str]) -> ParseResult<Self> {
115 match w.len() {
116 4 => {
119 let unit_type = w[0].parse::<UnitType>()?;
120 if unit_type != UnitType::Army {
121 Err(Error::new(ErrorKind::InvalidUnitType, w[0]))
122 } else {
123 Self::from_words(&w[1..])
124 }
125 }
126 3 => Ok(ConvoyedMove::new(w[0].parse()?, w[2].parse()?)),
127 _ => Err(Error::new(ErrorKind::MalformedConvoy, w.join(" "))),
128 }
129 }
130}
131
132impl<L: Location + FromStr<Err = Error>> FromWords for RetreatCommand<L> {
133 type Err = Error;
134
135 fn from_words(w: &[&str]) -> ParseResult<Self> {
136 match &w[0].to_lowercase()[..] {
137 "hold" | "holds" => Ok(RetreatCommand::Hold),
138 "->" => Ok(RetreatCommand::Move(w[1].parse()?)),
139 cmd => Err(Error::new(ErrorKind::UnknownCommand, cmd)),
140 }
141 }
142}
143
144impl FromWords for BuildCommand {
145 type Err = Error;
146
147 fn from_words(w: &[&str]) -> ParseResult<Self> {
148 match &w[0].to_lowercase()[..] {
149 "build" => Ok(BuildCommand::Build),
150 "disband" => Ok(BuildCommand::Disband),
151 cmd => Err(Error::new(ErrorKind::UnknownCommand, cmd)),
152 }
153 }
154}
155
156#[cfg(test)]
157mod test {
158 use super::*;
159 use crate::geo::RegionKey;
160 use crate::order::{MainCommand, Order};
161
162 type OrderParseResult = Result<Order<RegionKey, MainCommand<RegionKey>>, Error>;
163
164 #[test]
165 fn hold() {
166 let h_order: OrderParseResult = "AUS: F Tri hold".parse();
167 println!("{}", h_order.unwrap());
168 }
169
170 #[test]
171 fn army_move() {
172 let m_order: OrderParseResult = "ENG: A Lon -> Bel".parse();
173 println!("{}", m_order.unwrap());
174 }
175
176 #[test]
177 fn army_move_via_convoy() {
178 let m_order: OrderParseResult = "ENG: A Lon -> Bel via convoy".parse();
179 let order = m_order.unwrap();
180 assert_eq!(
181 order.command.move_dest(),
182 Some(&RegionKey::new("Bel", None))
183 );
184
185 let alt_casing: OrderParseResult = "ENG: A Lon -> Bel via Convoy".parse();
186 assert_eq!(alt_casing.unwrap(), order);
187
188 let no_pref: OrderParseResult = "ENG: A Lon -> Bel".parse();
189 assert_ne!(no_pref.unwrap(), order);
190 }
191
192 #[test]
193 fn convoy_with_unit_type() {
194 let c_order: OrderParseResult = "ENG: F Lon convoys A Bel -> NTH".parse();
195 c_order.unwrap();
196 }
197
198 #[test]
199 fn convoy_without_unit_type() {
200 let c_order: OrderParseResult = "ENG: F Lon convoys Bel -> NTH".parse();
201 c_order.unwrap();
202 }
203}