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: CatalogObjectTextError,
21) -> Result<String, CatalogObjectTextError> {
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 CatalogObjectTextError {
33 EmptyCatalogName,
34 EmptyCatalogObjectId,
35 EmptyDesignation,
36}
37
38impl fmt::Display for CatalogObjectTextError {
39 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40 match self {
41 Self::EmptyCatalogName => formatter.write_str("catalog name cannot be empty"),
42 Self::EmptyCatalogObjectId => {
43 formatter.write_str("catalog object identifier cannot be empty")
44 },
45 Self::EmptyDesignation => formatter.write_str("catalog designation cannot be empty"),
46 }
47 }
48}
49
50impl Error for CatalogObjectTextError {}
51
52#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
53pub struct CatalogName(String);
54
55impl CatalogName {
56 pub fn new(value: impl AsRef<str>) -> Result<Self, CatalogObjectTextError> {
62 non_empty_text(value, CatalogObjectTextError::EmptyCatalogName).map(Self)
63 }
64
65 #[must_use]
66 pub fn as_str(&self) -> &str {
67 &self.0
68 }
69}
70
71impl AsRef<str> for CatalogName {
72 fn as_ref(&self) -> &str {
73 self.as_str()
74 }
75}
76
77impl fmt::Display for CatalogName {
78 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79 formatter.write_str(self.as_str())
80 }
81}
82
83impl FromStr for CatalogName {
84 type Err = CatalogObjectTextError;
85
86 fn from_str(value: &str) -> Result<Self, Self::Err> {
87 Self::new(value)
88 }
89}
90
91#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
92pub struct CatalogObjectId(String);
93
94impl CatalogObjectId {
95 pub fn new(value: impl AsRef<str>) -> Result<Self, CatalogObjectTextError> {
101 non_empty_text(value, CatalogObjectTextError::EmptyCatalogObjectId).map(Self)
102 }
103
104 #[must_use]
105 pub fn as_str(&self) -> &str {
106 &self.0
107 }
108}
109
110impl AsRef<str> for CatalogObjectId {
111 fn as_ref(&self) -> &str {
112 self.as_str()
113 }
114}
115
116impl fmt::Display for CatalogObjectId {
117 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
118 formatter.write_str(self.as_str())
119 }
120}
121
122impl FromStr for CatalogObjectId {
123 type Err = CatalogObjectTextError;
124
125 fn from_str(value: &str) -> Result<Self, Self::Err> {
126 Self::new(value)
127 }
128}
129
130#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
131pub struct CatalogDesignation(String);
132
133impl CatalogDesignation {
134 pub fn new(value: impl AsRef<str>) -> Result<Self, CatalogObjectTextError> {
140 non_empty_text(value, CatalogObjectTextError::EmptyDesignation).map(Self)
141 }
142
143 #[must_use]
144 pub fn as_str(&self) -> &str {
145 &self.0
146 }
147}
148
149impl AsRef<str> for CatalogDesignation {
150 fn as_ref(&self) -> &str {
151 self.as_str()
152 }
153}
154
155impl fmt::Display for CatalogDesignation {
156 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
157 formatter.write_str(self.as_str())
158 }
159}
160
161impl FromStr for CatalogDesignation {
162 type Err = CatalogObjectTextError;
163
164 fn from_str(value: &str) -> Result<Self, Self::Err> {
165 Self::new(value)
166 }
167}
168
169#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
170pub enum CatalogObjectKind {
171 Messier,
172 Ngc,
173 Ic,
174 Hipparcos,
175 Gaia,
176 HenryDraper,
177 Simbad,
178 ExoplanetCatalog,
179 Unknown,
180 Custom(String),
181}
182
183impl fmt::Display for CatalogObjectKind {
184 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
185 match self {
186 Self::Messier => formatter.write_str("messier"),
187 Self::Ngc => formatter.write_str("ngc"),
188 Self::Ic => formatter.write_str("ic"),
189 Self::Hipparcos => formatter.write_str("hipparcos"),
190 Self::Gaia => formatter.write_str("gaia"),
191 Self::HenryDraper => formatter.write_str("henry-draper"),
192 Self::Simbad => formatter.write_str("simbad"),
193 Self::ExoplanetCatalog => formatter.write_str("exoplanet-catalog"),
194 Self::Unknown => formatter.write_str("unknown"),
195 Self::Custom(value) => formatter.write_str(value),
196 }
197 }
198}
199
200#[derive(Clone, Copy, Debug, Eq, PartialEq)]
201pub enum CatalogObjectKindParseError {
202 Empty,
203}
204
205impl fmt::Display for CatalogObjectKindParseError {
206 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
207 match self {
208 Self::Empty => formatter.write_str("catalog object kind cannot be empty"),
209 }
210 }
211}
212
213impl Error for CatalogObjectKindParseError {}
214
215impl FromStr for CatalogObjectKind {
216 type Err = CatalogObjectKindParseError;
217
218 fn from_str(value: &str) -> Result<Self, Self::Err> {
219 let trimmed = value.trim();
220
221 if trimmed.is_empty() {
222 return Err(CatalogObjectKindParseError::Empty);
223 }
224
225 match normalized_key(trimmed).as_str() {
226 "messier" => Ok(Self::Messier),
227 "ngc" => Ok(Self::Ngc),
228 "ic" => Ok(Self::Ic),
229 "hipparcos" => Ok(Self::Hipparcos),
230 "gaia" => Ok(Self::Gaia),
231 "henry-draper" | "henrydraper" => Ok(Self::HenryDraper),
232 "simbad" => Ok(Self::Simbad),
233 "exoplanet-catalog" | "exoplanetcatalog" => Ok(Self::ExoplanetCatalog),
234 "unknown" => Ok(Self::Unknown),
235 _ => Ok(Self::Custom(trimmed.to_string())),
236 }
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::{
243 CatalogDesignation, CatalogName, CatalogObjectId, CatalogObjectKind, CatalogObjectTextError,
244 };
245
246 #[test]
247 fn valid_catalog_name() {
248 let name = CatalogName::new("Messier").unwrap();
249
250 assert_eq!(name.as_str(), "Messier");
251 }
252
253 #[test]
254 fn empty_catalog_name_rejected() {
255 assert_eq!(
256 CatalogName::new(" "),
257 Err(CatalogObjectTextError::EmptyCatalogName)
258 );
259 }
260
261 #[test]
262 fn valid_catalog_object_id() {
263 let object_id = CatalogObjectId::new("031").unwrap();
264
265 assert_eq!(object_id.as_str(), "031");
266 }
267
268 #[test]
269 fn empty_catalog_object_id_rejected() {
270 assert_eq!(
271 CatalogObjectId::new(" "),
272 Err(CatalogObjectTextError::EmptyCatalogObjectId)
273 );
274 }
275
276 #[test]
277 fn catalog_object_kind_display_and_parse() {
278 assert_eq!(CatalogObjectKind::Messier.to_string(), "messier");
279 assert_eq!(
280 "ngc".parse::<CatalogObjectKind>().unwrap(),
281 CatalogObjectKind::Ngc
282 );
283 }
284
285 #[test]
286 fn custom_catalog_object_kind() {
287 assert_eq!(
288 "caldwell".parse::<CatalogObjectKind>().unwrap(),
289 CatalogObjectKind::Custom("caldwell".to_string())
290 );
291 }
292
293 #[test]
294 fn designation_construction() {
295 let designation = CatalogDesignation::new("Messier 31").unwrap();
296
297 assert_eq!(designation.as_str(), "Messier 31");
298 }
299}