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