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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum AtmosphericConditionError {
24 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub enum AtmosphereLayer {
41 Troposphere,
43 Stratosphere,
45 Mesosphere,
47 Thermosphere,
49 Exosphere,
51 Unknown,
53 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95pub enum AtmosphereLayerParseError {
96 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
112pub enum AirMassKind {
113 ContinentalPolar,
115 ContinentalTropical,
117 MaritimePolar,
119 MaritimeTropical,
121 Arctic,
123 Antarctic,
125 Unknown,
127 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
171pub enum AirMassKindParseError {
172 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
188pub enum VisibilityCondition {
189 Clear,
191 Haze,
193 Mist,
195 Fog,
197 Smoke,
199 Dust,
201 Unknown,
203 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
247pub enum VisibilityConditionParseError {
248 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
264pub struct AtmosphericCondition(String);
265
266impl AtmosphericCondition {
267 pub fn new(value: impl AsRef<str>) -> Result<Self, AtmosphericConditionError> {
273 non_empty_text(value).map(Self)
274 }
275
276 #[must_use]
278 pub fn as_str(&self) -> &str {
279 &self.0
280 }
281
282 #[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}