Skip to main content

use_planet/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn normalized_key(value: &str) -> String {
8    value
9        .trim()
10        .chars()
11        .map(|character| match character {
12            '_' | ' ' => '-',
13            other => other.to_ascii_lowercase(),
14        })
15        .collect()
16}
17
18fn non_empty_text(
19    value: impl AsRef<str>,
20    error: PlanetTextError,
21) -> Result<String, PlanetTextError> {
22    let trimmed = value.as_ref().trim();
23
24    if trimmed.is_empty() {
25        Err(error)
26    } else {
27        Ok(trimmed.to_string())
28    }
29}
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub enum PlanetTextError {
33    EmptyName,
34    EmptySystemName,
35}
36
37impl fmt::Display for PlanetTextError {
38    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            Self::EmptyName => formatter.write_str("planet name cannot be empty"),
41            Self::EmptySystemName => formatter.write_str("planetary system name cannot be empty"),
42        }
43    }
44}
45
46impl Error for PlanetTextError {}
47
48#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
49pub struct PlanetName(String);
50
51impl PlanetName {
52    /// Creates a planet name from non-empty text.
53    ///
54    /// # Errors
55    ///
56    /// Returns [`PlanetTextError::EmptyName`] when the trimmed input is empty.
57    pub fn new(value: impl AsRef<str>) -> Result<Self, PlanetTextError> {
58        non_empty_text(value, PlanetTextError::EmptyName).map(Self)
59    }
60
61    #[must_use]
62    pub fn as_str(&self) -> &str {
63        &self.0
64    }
65
66    #[must_use]
67    pub fn into_string(self) -> String {
68        self.0
69    }
70}
71
72impl AsRef<str> for PlanetName {
73    fn as_ref(&self) -> &str {
74        self.as_str()
75    }
76}
77
78impl fmt::Display for PlanetName {
79    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80        formatter.write_str(self.as_str())
81    }
82}
83
84impl FromStr for PlanetName {
85    type Err = PlanetTextError;
86
87    fn from_str(value: &str) -> Result<Self, Self::Err> {
88        Self::new(value)
89    }
90}
91
92#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
93pub struct PlanetarySystemName(String);
94
95impl PlanetarySystemName {
96    /// Creates a planetary system name from non-empty text.
97    ///
98    /// # Errors
99    ///
100    /// Returns [`PlanetTextError::EmptySystemName`] when the trimmed input is empty.
101    pub fn new(value: impl AsRef<str>) -> Result<Self, PlanetTextError> {
102        non_empty_text(value, PlanetTextError::EmptySystemName).map(Self)
103    }
104
105    #[must_use]
106    pub fn as_str(&self) -> &str {
107        &self.0
108    }
109
110    #[must_use]
111    pub fn into_string(self) -> String {
112        self.0
113    }
114}
115
116impl AsRef<str> for PlanetarySystemName {
117    fn as_ref(&self) -> &str {
118        self.as_str()
119    }
120}
121
122impl fmt::Display for PlanetarySystemName {
123    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
124        formatter.write_str(self.as_str())
125    }
126}
127
128impl FromStr for PlanetarySystemName {
129    type Err = PlanetTextError;
130
131    fn from_str(value: &str) -> Result<Self, Self::Err> {
132        Self::new(value)
133    }
134}
135
136#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
137pub enum PlanetKind {
138    Terrestrial,
139    GasGiant,
140    IceGiant,
141    DwarfPlanet,
142    Exoplanet,
143    RoguePlanet,
144    Unknown,
145    Custom(String),
146}
147
148impl fmt::Display for PlanetKind {
149    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
150        match self {
151            Self::Terrestrial => formatter.write_str("terrestrial"),
152            Self::GasGiant => formatter.write_str("gas-giant"),
153            Self::IceGiant => formatter.write_str("ice-giant"),
154            Self::DwarfPlanet => formatter.write_str("dwarf-planet"),
155            Self::Exoplanet => formatter.write_str("exoplanet"),
156            Self::RoguePlanet => formatter.write_str("rogue-planet"),
157            Self::Unknown => formatter.write_str("unknown"),
158            Self::Custom(value) => formatter.write_str(value),
159        }
160    }
161}
162
163#[derive(Clone, Copy, Debug, Eq, PartialEq)]
164pub enum PlanetKindParseError {
165    Empty,
166}
167
168impl fmt::Display for PlanetKindParseError {
169    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
170        match self {
171            Self::Empty => formatter.write_str("planet kind cannot be empty"),
172        }
173    }
174}
175
176impl Error for PlanetKindParseError {}
177
178impl FromStr for PlanetKind {
179    type Err = PlanetKindParseError;
180
181    fn from_str(value: &str) -> Result<Self, Self::Err> {
182        let trimmed = value.trim();
183
184        if trimmed.is_empty() {
185            return Err(PlanetKindParseError::Empty);
186        }
187
188        match normalized_key(trimmed).as_str() {
189            "terrestrial" => Ok(Self::Terrestrial),
190            "gas-giant" | "gasgiant" => Ok(Self::GasGiant),
191            "ice-giant" | "icegiant" => Ok(Self::IceGiant),
192            "dwarf-planet" | "dwarfplanet" => Ok(Self::DwarfPlanet),
193            "exoplanet" => Ok(Self::Exoplanet),
194            "rogue-planet" | "rogueplanet" => Ok(Self::RoguePlanet),
195            "unknown" => Ok(Self::Unknown),
196            _ => Ok(Self::Custom(trimmed.to_string())),
197        }
198    }
199}
200
201#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
202pub enum PlanetStatus {
203    Confirmed,
204    Candidate,
205    Provisional,
206    Disputed,
207    Unknown,
208    Custom(String),
209}
210
211impl fmt::Display for PlanetStatus {
212    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
213        match self {
214            Self::Confirmed => formatter.write_str("confirmed"),
215            Self::Candidate => formatter.write_str("candidate"),
216            Self::Provisional => formatter.write_str("provisional"),
217            Self::Disputed => formatter.write_str("disputed"),
218            Self::Unknown => formatter.write_str("unknown"),
219            Self::Custom(value) => formatter.write_str(value),
220        }
221    }
222}
223
224#[derive(Clone, Copy, Debug, Eq, PartialEq)]
225pub enum PlanetStatusParseError {
226    Empty,
227}
228
229impl fmt::Display for PlanetStatusParseError {
230    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
231        match self {
232            Self::Empty => formatter.write_str("planet status cannot be empty"),
233        }
234    }
235}
236
237impl Error for PlanetStatusParseError {}
238
239impl FromStr for PlanetStatus {
240    type Err = PlanetStatusParseError;
241
242    fn from_str(value: &str) -> Result<Self, Self::Err> {
243        let trimmed = value.trim();
244
245        if trimmed.is_empty() {
246            return Err(PlanetStatusParseError::Empty);
247        }
248
249        match normalized_key(trimmed).as_str() {
250            "confirmed" => Ok(Self::Confirmed),
251            "candidate" => Ok(Self::Candidate),
252            "provisional" => Ok(Self::Provisional),
253            "disputed" => Ok(Self::Disputed),
254            "unknown" => Ok(Self::Unknown),
255            _ => Ok(Self::Custom(trimmed.to_string())),
256        }
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::{PlanetKind, PlanetName, PlanetStatus, PlanetTextError, PlanetarySystemName};
263
264    #[test]
265    fn valid_planet_name() {
266        let name = PlanetName::new("Mercury").unwrap();
267
268        assert_eq!(name.as_str(), "Mercury");
269    }
270
271    #[test]
272    fn empty_planet_name_rejected() {
273        assert_eq!(PlanetName::new("  "), Err(PlanetTextError::EmptyName));
274    }
275
276    #[test]
277    fn planet_kind_display_and_parse() {
278        assert_eq!(PlanetKind::GasGiant.to_string(), "gas-giant");
279        assert_eq!(
280            "ice giant".parse::<PlanetKind>().unwrap(),
281            PlanetKind::IceGiant
282        );
283    }
284
285    #[test]
286    fn planet_status_display_and_parse() {
287        assert_eq!(PlanetStatus::Confirmed.to_string(), "confirmed");
288        assert_eq!(
289            "disputed".parse::<PlanetStatus>().unwrap(),
290            PlanetStatus::Disputed
291        );
292    }
293
294    #[test]
295    fn custom_planet_kind() {
296        assert_eq!(
297            "super-puff".parse::<PlanetKind>().unwrap(),
298            PlanetKind::Custom("super-puff".to_string())
299        );
300    }
301
302    #[test]
303    fn planetary_system_name_construction() {
304        let system = PlanetarySystemName::new("TRAPPIST-1").unwrap();
305
306        assert_eq!(system.as_str(), "TRAPPIST-1");
307    }
308}