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 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 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}