Skip to main content

use_moon/
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(value: impl AsRef<str>, error: MoonTextError) -> Result<String, MoonTextError> {
19    let trimmed = value.as_ref().trim();
20
21    if trimmed.is_empty() {
22        Err(error)
23    } else {
24        Ok(trimmed.to_string())
25    }
26}
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub enum MoonTextError {
30    EmptyName,
31    EmptyParentIdentifier,
32    EmptySatelliteIdentifier,
33}
34
35impl fmt::Display for MoonTextError {
36    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
37        match self {
38            Self::EmptyName => formatter.write_str("moon name cannot be empty"),
39            Self::EmptyParentIdentifier => formatter.write_str("parent identifier cannot be empty"),
40            Self::EmptySatelliteIdentifier => {
41                formatter.write_str("satellite identifier cannot be empty")
42            },
43        }
44    }
45}
46
47impl Error for MoonTextError {}
48
49#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
50pub struct MoonName(String);
51
52impl MoonName {
53    /// Creates a moon name from non-empty text.
54    ///
55    /// # Errors
56    ///
57    /// Returns [`MoonTextError::EmptyName`] when the trimmed input is empty.
58    pub fn new(value: impl AsRef<str>) -> Result<Self, MoonTextError> {
59        non_empty_text(value, MoonTextError::EmptyName).map(Self)
60    }
61
62    #[must_use]
63    pub fn as_str(&self) -> &str {
64        &self.0
65    }
66}
67
68impl AsRef<str> for MoonName {
69    fn as_ref(&self) -> &str {
70        self.as_str()
71    }
72}
73
74impl fmt::Display for MoonName {
75    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
76        formatter.write_str(self.as_str())
77    }
78}
79
80impl FromStr for MoonName {
81    type Err = MoonTextError;
82
83    fn from_str(value: &str) -> Result<Self, Self::Err> {
84        Self::new(value)
85    }
86}
87
88#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
89pub enum MoonKind {
90    Regular,
91    Irregular,
92    Captured,
93    Shepherd,
94    Trojan,
95    Unknown,
96    Custom(String),
97}
98
99impl fmt::Display for MoonKind {
100    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101        match self {
102            Self::Regular => formatter.write_str("regular"),
103            Self::Irregular => formatter.write_str("irregular"),
104            Self::Captured => formatter.write_str("captured"),
105            Self::Shepherd => formatter.write_str("shepherd"),
106            Self::Trojan => formatter.write_str("trojan"),
107            Self::Unknown => formatter.write_str("unknown"),
108            Self::Custom(value) => formatter.write_str(value),
109        }
110    }
111}
112
113#[derive(Clone, Copy, Debug, Eq, PartialEq)]
114pub enum MoonKindParseError {
115    Empty,
116}
117
118impl fmt::Display for MoonKindParseError {
119    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
120        match self {
121            Self::Empty => formatter.write_str("moon kind cannot be empty"),
122        }
123    }
124}
125
126impl Error for MoonKindParseError {}
127
128impl FromStr for MoonKind {
129    type Err = MoonKindParseError;
130
131    fn from_str(value: &str) -> Result<Self, Self::Err> {
132        let trimmed = value.trim();
133
134        if trimmed.is_empty() {
135            return Err(MoonKindParseError::Empty);
136        }
137
138        match normalized_key(trimmed).as_str() {
139            "regular" => Ok(Self::Regular),
140            "irregular" => Ok(Self::Irregular),
141            "captured" => Ok(Self::Captured),
142            "shepherd" => Ok(Self::Shepherd),
143            "trojan" => Ok(Self::Trojan),
144            "unknown" => Ok(Self::Unknown),
145            _ => Ok(Self::Custom(trimmed.to_string())),
146        }
147    }
148}
149
150#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
151pub struct SatelliteRelation {
152    parent_identifier: String,
153    satellite_identifier: String,
154}
155
156impl SatelliteRelation {
157    /// Creates a relation between non-empty parent and satellite identifiers.
158    ///
159    /// # Errors
160    ///
161    /// Returns [`MoonTextError::EmptyParentIdentifier`] when the trimmed parent identifier is empty,
162    /// or [`MoonTextError::EmptySatelliteIdentifier`] when the trimmed satellite identifier is empty.
163    pub fn new(
164        parent_identifier: impl AsRef<str>,
165        satellite_identifier: impl AsRef<str>,
166    ) -> Result<Self, MoonTextError> {
167        Ok(Self {
168            parent_identifier: non_empty_text(
169                parent_identifier,
170                MoonTextError::EmptyParentIdentifier,
171            )?,
172            satellite_identifier: non_empty_text(
173                satellite_identifier,
174                MoonTextError::EmptySatelliteIdentifier,
175            )?,
176        })
177    }
178
179    #[must_use]
180    pub fn parent_identifier(&self) -> &str {
181        &self.parent_identifier
182    }
183
184    #[must_use]
185    pub fn satellite_identifier(&self) -> &str {
186        &self.satellite_identifier
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::{MoonKind, MoonName, MoonTextError, SatelliteRelation};
193
194    #[test]
195    fn valid_moon_name() {
196        let name = MoonName::new("Europa").unwrap();
197
198        assert_eq!(name.as_str(), "Europa");
199    }
200
201    #[test]
202    fn empty_moon_name_rejected() {
203        assert_eq!(MoonName::new("   "), Err(MoonTextError::EmptyName));
204    }
205
206    #[test]
207    fn moon_kind_display_and_parse() {
208        assert_eq!(MoonKind::Regular.to_string(), "regular");
209        assert_eq!("captured".parse::<MoonKind>().unwrap(), MoonKind::Captured);
210    }
211
212    #[test]
213    fn custom_moon_kind() {
214        assert_eq!(
215            "resonant".parse::<MoonKind>().unwrap(),
216            MoonKind::Custom("resonant".to_string())
217        );
218    }
219
220    #[test]
221    fn satellite_relation_construction() {
222        let relation = SatelliteRelation::new("Jupiter", "Europa").unwrap();
223
224        assert_eq!(relation.parent_identifier(), "Jupiter");
225        assert_eq!(relation.satellite_identifier(), "Europa");
226    }
227}