Skip to main content

use_atmosphere/
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.trim().to_ascii_lowercase().replace(['_', ' '], "-")
9}
10
11fn non_empty_text(value: impl AsRef<str>) -> Result<String, AtmosphericConditionError> {
12    let trimmed = value.as_ref().trim();
13
14    if trimmed.is_empty() {
15        Err(AtmosphericConditionError::Empty)
16    } else {
17        Ok(trimmed.to_string())
18    }
19}
20
21/// Error returned when atmospheric condition text is empty.
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum AtmosphericConditionError {
24    /// The supplied condition label was empty after trimming whitespace.
25    Empty,
26}
27
28impl fmt::Display for AtmosphericConditionError {
29    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            Self::Empty => formatter.write_str("atmospheric condition cannot be empty"),
32        }
33    }
34}
35
36impl Error for AtmosphericConditionError {}
37
38/// Stable atmosphere layer vocabulary.
39#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub enum AtmosphereLayer {
41    /// Troposphere.
42    Troposphere,
43    /// Stratosphere.
44    Stratosphere,
45    /// Mesosphere.
46    Mesosphere,
47    /// Thermosphere.
48    Thermosphere,
49    /// Exosphere.
50    Exosphere,
51    /// Unknown atmosphere layer.
52    Unknown,
53    /// Caller-defined atmosphere layer.
54    Custom(String),
55}
56
57impl fmt::Display for AtmosphereLayer {
58    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match self {
60            Self::Troposphere => formatter.write_str("troposphere"),
61            Self::Stratosphere => formatter.write_str("stratosphere"),
62            Self::Mesosphere => formatter.write_str("mesosphere"),
63            Self::Thermosphere => formatter.write_str("thermosphere"),
64            Self::Exosphere => formatter.write_str("exosphere"),
65            Self::Unknown => formatter.write_str("unknown"),
66            Self::Custom(value) => formatter.write_str(value),
67        }
68    }
69}
70
71impl FromStr for AtmosphereLayer {
72    type Err = AtmosphereLayerParseError;
73
74    fn from_str(value: &str) -> Result<Self, Self::Err> {
75        let trimmed = value.trim();
76
77        if trimmed.is_empty() {
78            return Err(AtmosphereLayerParseError::Empty);
79        }
80
81        match normalized_key(trimmed).as_str() {
82            "troposphere" => Ok(Self::Troposphere),
83            "stratosphere" => Ok(Self::Stratosphere),
84            "mesosphere" => Ok(Self::Mesosphere),
85            "thermosphere" => Ok(Self::Thermosphere),
86            "exosphere" => Ok(Self::Exosphere),
87            "unknown" => Ok(Self::Unknown),
88            _ => Ok(Self::Custom(trimmed.to_string())),
89        }
90    }
91}
92
93/// Error returned when parsing atmosphere layers fails.
94#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95pub enum AtmosphereLayerParseError {
96    /// The atmosphere layer was empty after trimming whitespace.
97    Empty,
98}
99
100impl fmt::Display for AtmosphereLayerParseError {
101    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
102        match self {
103            Self::Empty => formatter.write_str("atmosphere layer cannot be empty"),
104        }
105    }
106}
107
108impl Error for AtmosphereLayerParseError {}
109
110/// Stable air-mass vocabulary.
111#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
112pub enum AirMassKind {
113    /// Continental polar.
114    ContinentalPolar,
115    /// Continental tropical.
116    ContinentalTropical,
117    /// Maritime polar.
118    MaritimePolar,
119    /// Maritime tropical.
120    MaritimeTropical,
121    /// Arctic.
122    Arctic,
123    /// Antarctic.
124    Antarctic,
125    /// Unknown air mass.
126    Unknown,
127    /// Caller-defined air-mass label.
128    Custom(String),
129}
130
131impl fmt::Display for AirMassKind {
132    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
133        match self {
134            Self::ContinentalPolar => formatter.write_str("continental-polar"),
135            Self::ContinentalTropical => formatter.write_str("continental-tropical"),
136            Self::MaritimePolar => formatter.write_str("maritime-polar"),
137            Self::MaritimeTropical => formatter.write_str("maritime-tropical"),
138            Self::Arctic => formatter.write_str("arctic"),
139            Self::Antarctic => formatter.write_str("antarctic"),
140            Self::Unknown => formatter.write_str("unknown"),
141            Self::Custom(value) => formatter.write_str(value),
142        }
143    }
144}
145
146impl FromStr for AirMassKind {
147    type Err = AirMassKindParseError;
148
149    fn from_str(value: &str) -> Result<Self, Self::Err> {
150        let trimmed = value.trim();
151
152        if trimmed.is_empty() {
153            return Err(AirMassKindParseError::Empty);
154        }
155
156        match normalized_key(trimmed).as_str() {
157            "continental-polar" => Ok(Self::ContinentalPolar),
158            "continental-tropical" => Ok(Self::ContinentalTropical),
159            "maritime-polar" => Ok(Self::MaritimePolar),
160            "maritime-tropical" => Ok(Self::MaritimeTropical),
161            "arctic" => Ok(Self::Arctic),
162            "antarctic" => Ok(Self::Antarctic),
163            "unknown" => Ok(Self::Unknown),
164            _ => Ok(Self::Custom(trimmed.to_string())),
165        }
166    }
167}
168
169/// Error returned when parsing air masses fails.
170#[derive(Clone, Copy, Debug, Eq, PartialEq)]
171pub enum AirMassKindParseError {
172    /// The air mass kind was empty after trimming whitespace.
173    Empty,
174}
175
176impl fmt::Display for AirMassKindParseError {
177    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
178        match self {
179            Self::Empty => formatter.write_str("air mass kind cannot be empty"),
180        }
181    }
182}
183
184impl Error for AirMassKindParseError {}
185
186/// Stable visibility condition vocabulary.
187#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
188pub enum VisibilityCondition {
189    /// Clear visibility.
190    Clear,
191    /// Haze.
192    Haze,
193    /// Mist.
194    Mist,
195    /// Fog.
196    Fog,
197    /// Smoke.
198    Smoke,
199    /// Dust.
200    Dust,
201    /// Unknown visibility condition.
202    Unknown,
203    /// Caller-defined visibility condition.
204    Custom(String),
205}
206
207impl fmt::Display for VisibilityCondition {
208    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
209        match self {
210            Self::Clear => formatter.write_str("clear"),
211            Self::Haze => formatter.write_str("haze"),
212            Self::Mist => formatter.write_str("mist"),
213            Self::Fog => formatter.write_str("fog"),
214            Self::Smoke => formatter.write_str("smoke"),
215            Self::Dust => formatter.write_str("dust"),
216            Self::Unknown => formatter.write_str("unknown"),
217            Self::Custom(value) => formatter.write_str(value),
218        }
219    }
220}
221
222impl FromStr for VisibilityCondition {
223    type Err = VisibilityConditionParseError;
224
225    fn from_str(value: &str) -> Result<Self, Self::Err> {
226        let trimmed = value.trim();
227
228        if trimmed.is_empty() {
229            return Err(VisibilityConditionParseError::Empty);
230        }
231
232        match normalized_key(trimmed).as_str() {
233            "clear" => Ok(Self::Clear),
234            "haze" => Ok(Self::Haze),
235            "mist" => Ok(Self::Mist),
236            "fog" => Ok(Self::Fog),
237            "smoke" => Ok(Self::Smoke),
238            "dust" => Ok(Self::Dust),
239            "unknown" => Ok(Self::Unknown),
240            _ => Ok(Self::Custom(trimmed.to_string())),
241        }
242    }
243}
244
245/// Error returned when parsing visibility conditions fails.
246#[derive(Clone, Copy, Debug, Eq, PartialEq)]
247pub enum VisibilityConditionParseError {
248    /// The visibility condition was empty after trimming whitespace.
249    Empty,
250}
251
252impl fmt::Display for VisibilityConditionParseError {
253    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
254        match self {
255            Self::Empty => formatter.write_str("visibility condition cannot be empty"),
256        }
257    }
258}
259
260impl Error for VisibilityConditionParseError {}
261
262/// A descriptive non-empty atmospheric condition label.
263#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
264pub struct AtmosphericCondition(String);
265
266impl AtmosphericCondition {
267    /// Creates an atmospheric condition label from non-empty text.
268    ///
269    /// # Errors
270    ///
271    /// Returns [`AtmosphericConditionError::Empty`] when the label is empty.
272    pub fn new(value: impl AsRef<str>) -> Result<Self, AtmosphericConditionError> {
273        non_empty_text(value).map(Self)
274    }
275
276    /// Returns the stored atmospheric condition text.
277    #[must_use]
278    pub fn as_str(&self) -> &str {
279        &self.0
280    }
281
282    /// Consumes the condition and returns the owned string.
283    #[must_use]
284    pub fn into_string(self) -> String {
285        self.0
286    }
287}
288
289impl AsRef<str> for AtmosphericCondition {
290    fn as_ref(&self) -> &str {
291        self.as_str()
292    }
293}
294
295impl fmt::Display for AtmosphericCondition {
296    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
297        formatter.write_str(self.as_str())
298    }
299}
300
301impl FromStr for AtmosphericCondition {
302    type Err = AtmosphericConditionError;
303
304    fn from_str(value: &str) -> Result<Self, Self::Err> {
305        Self::new(value)
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::{
312        AirMassKind, AirMassKindParseError, AtmosphereLayer, AtmosphereLayerParseError,
313        AtmosphericCondition, AtmosphericConditionError, VisibilityCondition,
314        VisibilityConditionParseError,
315    };
316    use core::str::FromStr;
317
318    #[test]
319    fn atmosphere_layer_display_and_parse() {
320        assert_eq!(AtmosphereLayer::Stratosphere.to_string(), "stratosphere");
321        assert_eq!(
322            AtmosphereLayer::from_str("mesosphere").unwrap(),
323            AtmosphereLayer::Mesosphere
324        );
325        assert_eq!(
326            AtmosphereLayer::from_str(" "),
327            Err(AtmosphereLayerParseError::Empty)
328        );
329    }
330
331    #[test]
332    fn air_mass_display_and_parse() {
333        assert_eq!(AirMassKind::MaritimePolar.to_string(), "maritime-polar");
334        assert_eq!(
335            AirMassKind::from_str("continental tropical").unwrap(),
336            AirMassKind::ContinentalTropical
337        );
338        assert_eq!(
339            AirMassKind::from_str(" "),
340            Err(AirMassKindParseError::Empty)
341        );
342    }
343
344    #[test]
345    fn visibility_condition_display_and_parse() {
346        assert_eq!(VisibilityCondition::Fog.to_string(), "fog");
347        assert_eq!(
348            VisibilityCondition::from_str("smoke").unwrap(),
349            VisibilityCondition::Smoke
350        );
351        assert_eq!(
352            VisibilityCondition::from_str(" "),
353            Err(VisibilityConditionParseError::Empty)
354        );
355    }
356
357    #[test]
358    fn custom_atmosphere_layer() {
359        assert_eq!(
360            AtmosphereLayer::from_str("planetary boundary layer").unwrap(),
361            AtmosphereLayer::Custom(String::from("planetary boundary layer"))
362        );
363    }
364
365    #[test]
366    fn atmospheric_condition_requires_text() {
367        assert_eq!(
368            AtmosphericCondition::new("   "),
369            Err(AtmosphericConditionError::Empty)
370        );
371        assert_eq!(
372            AtmosphericCondition::new("unstable low levels")
373                .unwrap()
374                .as_str(),
375            "unstable low levels"
376        );
377    }
378}