Skip to main content

diplomacy/parser/
mod.rs

1//! Contains error types and trait implementations for parsing diplomacy orders.
2//!
3//! All orders are expected to be in the following format:
4//!
5//! ```text
6//! {Nation}: {UnitType} {Location} {Command}
7//! ```
8//!
9//! # Supported Commands
10//! 1. Hold: `hold` or `holds`
11//! 1. Move: `-> {Destination}`
12//! 1. Support: `supports {UnitType} {Region} [-> {Dest}]`
13//! 1. Convoy: `convoys {Region} -> {Dest}`
14//! 1. Build: `build` (this is non-idiomatic, but easier to parse)
15//! 1. Disband: `disband`
16
17use 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
30/// A parser which operates on whitespace-delimited words from an input string.
31pub trait FromWords: Sized {
32    /// The associated error which can be returned from parsing.
33    type Err;
34
35    /// Performs the conversion.
36    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            // {unitType} {in}
99            2 => Ok(SupportedOrder::Hold(w[0].parse()?, w[1].parse()?)),
100            // {unitType} {from} -> {to}
101            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            // The unit type has to be army, so make it optional. If a unit type is declared,
117            // check that it's valid.
118            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}