Skip to main content

use_catalog_object/
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: 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    /// Creates a catalog name from non-empty text.
57    ///
58    /// # Errors
59    ///
60    /// Returns [`CatalogObjectTextError::EmptyCatalogName`] when the trimmed input is empty.
61    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    /// Creates a catalog object identifier from non-empty text.
96    ///
97    /// # Errors
98    ///
99    /// Returns [`CatalogObjectTextError::EmptyCatalogObjectId`] when the trimmed input is empty.
100    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    /// Creates a catalog designation from non-empty text.
135    ///
136    /// # Errors
137    ///
138    /// Returns [`CatalogObjectTextError::EmptyDesignation`] when the trimmed input is empty.
139    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}