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(
12 value: impl AsRef<str>,
13 error: WeatherObservationError,
14) -> Result<String, WeatherObservationError> {
15 let trimmed = value.as_ref().trim();
16
17 if trimmed.is_empty() {
18 Err(error)
19 } else {
20 Ok(trimmed.to_string())
21 }
22}
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum WeatherObservationError {
27 EmptyObservationId,
29 EmptyObservationSource,
31}
32
33impl fmt::Display for WeatherObservationError {
34 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 Self::EmptyObservationId => {
37 formatter.write_str("weather observation identifier cannot be empty")
38 },
39 Self::EmptyObservationSource => {
40 formatter.write_str("weather observation source cannot be empty")
41 },
42 }
43 }
44}
45
46impl Error for WeatherObservationError {}
47
48#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
50pub struct WeatherObservationId(String);
51
52impl WeatherObservationId {
53 pub fn new(value: impl AsRef<str>) -> Result<Self, WeatherObservationError> {
59 non_empty_text(value, WeatherObservationError::EmptyObservationId).map(Self)
60 }
61
62 #[must_use]
64 pub fn as_str(&self) -> &str {
65 &self.0
66 }
67
68 #[must_use]
70 pub fn into_string(self) -> String {
71 self.0
72 }
73}
74
75impl AsRef<str> for WeatherObservationId {
76 fn as_ref(&self) -> &str {
77 self.as_str()
78 }
79}
80
81impl fmt::Display for WeatherObservationId {
82 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83 formatter.write_str(self.as_str())
84 }
85}
86
87impl FromStr for WeatherObservationId {
88 type Err = WeatherObservationError;
89
90 fn from_str(value: &str) -> Result<Self, Self::Err> {
91 Self::new(value)
92 }
93}
94
95#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
97pub struct ObservationSource(String);
98
99impl ObservationSource {
100 pub fn new(value: impl AsRef<str>) -> Result<Self, WeatherObservationError> {
106 non_empty_text(value, WeatherObservationError::EmptyObservationSource).map(Self)
107 }
108
109 #[must_use]
111 pub fn as_str(&self) -> &str {
112 &self.0
113 }
114
115 #[must_use]
117 pub fn into_string(self) -> String {
118 self.0
119 }
120}
121
122impl AsRef<str> for ObservationSource {
123 fn as_ref(&self) -> &str {
124 self.as_str()
125 }
126}
127
128impl fmt::Display for ObservationSource {
129 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
130 formatter.write_str(self.as_str())
131 }
132}
133
134impl FromStr for ObservationSource {
135 type Err = WeatherObservationError;
136
137 fn from_str(value: &str) -> Result<Self, Self::Err> {
138 Self::new(value)
139 }
140}
141
142#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
144pub enum ObservationKind {
145 Surface,
147 UpperAir,
149 Radar,
151 Satellite,
153 Buoy,
155 Station,
157 Manual,
159 Automated,
161 Unknown,
163 Custom(String),
165}
166
167impl fmt::Display for ObservationKind {
168 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
169 match self {
170 Self::Surface => formatter.write_str("surface"),
171 Self::UpperAir => formatter.write_str("upper-air"),
172 Self::Radar => formatter.write_str("radar"),
173 Self::Satellite => formatter.write_str("satellite"),
174 Self::Buoy => formatter.write_str("buoy"),
175 Self::Station => formatter.write_str("station"),
176 Self::Manual => formatter.write_str("manual"),
177 Self::Automated => formatter.write_str("automated"),
178 Self::Unknown => formatter.write_str("unknown"),
179 Self::Custom(value) => formatter.write_str(value),
180 }
181 }
182}
183
184impl FromStr for ObservationKind {
185 type Err = ObservationKindParseError;
186
187 fn from_str(value: &str) -> Result<Self, Self::Err> {
188 let trimmed = value.trim();
189
190 if trimmed.is_empty() {
191 return Err(ObservationKindParseError::Empty);
192 }
193
194 match normalized_key(trimmed).as_str() {
195 "surface" => Ok(Self::Surface),
196 "upper-air" | "upperair" => Ok(Self::UpperAir),
197 "radar" => Ok(Self::Radar),
198 "satellite" => Ok(Self::Satellite),
199 "buoy" => Ok(Self::Buoy),
200 "station" => Ok(Self::Station),
201 "manual" => Ok(Self::Manual),
202 "automated" => Ok(Self::Automated),
203 "unknown" => Ok(Self::Unknown),
204 _ => Ok(Self::Custom(trimmed.to_string())),
205 }
206 }
207}
208
209#[derive(Clone, Copy, Debug, Eq, PartialEq)]
211pub enum ObservationKindParseError {
212 Empty,
214}
215
216impl fmt::Display for ObservationKindParseError {
217 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
218 match self {
219 Self::Empty => formatter.write_str("observation kind cannot be empty"),
220 }
221 }
222}
223
224impl Error for ObservationKindParseError {}
225
226#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
228pub enum ObservationQuality {
229 Raw,
231 Estimated,
233 Corrected,
235 Verified,
237 Questionable,
239 Missing,
241 Unknown,
243 Custom(String),
245}
246
247impl fmt::Display for ObservationQuality {
248 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
249 match self {
250 Self::Raw => formatter.write_str("raw"),
251 Self::Estimated => formatter.write_str("estimated"),
252 Self::Corrected => formatter.write_str("corrected"),
253 Self::Verified => formatter.write_str("verified"),
254 Self::Questionable => formatter.write_str("questionable"),
255 Self::Missing => formatter.write_str("missing"),
256 Self::Unknown => formatter.write_str("unknown"),
257 Self::Custom(value) => formatter.write_str(value),
258 }
259 }
260}
261
262impl FromStr for ObservationQuality {
263 type Err = ObservationQualityParseError;
264
265 fn from_str(value: &str) -> Result<Self, Self::Err> {
266 let trimmed = value.trim();
267
268 if trimmed.is_empty() {
269 return Err(ObservationQualityParseError::Empty);
270 }
271
272 match normalized_key(trimmed).as_str() {
273 "raw" => Ok(Self::Raw),
274 "estimated" => Ok(Self::Estimated),
275 "corrected" => Ok(Self::Corrected),
276 "verified" => Ok(Self::Verified),
277 "questionable" => Ok(Self::Questionable),
278 "missing" => Ok(Self::Missing),
279 "unknown" => Ok(Self::Unknown),
280 _ => Ok(Self::Custom(trimmed.to_string())),
281 }
282 }
283}
284
285#[derive(Clone, Copy, Debug, Eq, PartialEq)]
287pub enum ObservationQualityParseError {
288 Empty,
290}
291
292impl fmt::Display for ObservationQualityParseError {
293 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
294 match self {
295 Self::Empty => formatter.write_str("observation quality cannot be empty"),
296 }
297 }
298}
299
300impl Error for ObservationQualityParseError {}
301
302#[derive(Clone, Debug, Eq, PartialEq)]
304pub struct WeatherObservation {
305 id: WeatherObservationId,
306 kind: ObservationKind,
307 source: ObservationSource,
308 quality: ObservationQuality,
309}
310
311impl WeatherObservation {
312 #[must_use]
314 pub fn new(
315 id: WeatherObservationId,
316 kind: ObservationKind,
317 source: ObservationSource,
318 quality: ObservationQuality,
319 ) -> Self {
320 Self {
321 id,
322 kind,
323 source,
324 quality,
325 }
326 }
327
328 #[must_use]
330 pub fn id(&self) -> &WeatherObservationId {
331 &self.id
332 }
333
334 #[must_use]
336 pub fn kind(&self) -> &ObservationKind {
337 &self.kind
338 }
339
340 #[must_use]
342 pub fn source(&self) -> &ObservationSource {
343 &self.source
344 }
345
346 #[must_use]
348 pub fn quality(&self) -> &ObservationQuality {
349 &self.quality
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::{
356 ObservationKind, ObservationKindParseError, ObservationQuality,
357 ObservationQualityParseError, ObservationSource, WeatherObservation,
358 WeatherObservationError, WeatherObservationId,
359 };
360 use core::str::FromStr;
361
362 #[test]
363 fn valid_observation_id() {
364 let identifier = WeatherObservationId::new(" obs-001 ").unwrap();
365
366 assert_eq!(identifier.as_str(), "obs-001");
367 }
368
369 #[test]
370 fn empty_observation_id_rejected() {
371 assert_eq!(
372 WeatherObservationId::new(" "),
373 Err(WeatherObservationError::EmptyObservationId)
374 );
375 }
376
377 #[test]
378 fn observation_kind_display_and_parse() {
379 assert_eq!(ObservationKind::UpperAir.to_string(), "upper-air");
380 assert_eq!(
381 ObservationKind::from_str("upper air").unwrap(),
382 ObservationKind::UpperAir
383 );
384 assert_eq!(
385 ObservationKind::from_str(" "),
386 Err(ObservationKindParseError::Empty)
387 );
388 }
389
390 #[test]
391 fn observation_quality_display_and_parse() {
392 assert_eq!(ObservationQuality::Questionable.to_string(), "questionable");
393 assert_eq!(
394 ObservationQuality::from_str("verified").unwrap(),
395 ObservationQuality::Verified
396 );
397 assert_eq!(
398 ObservationQuality::from_str(" "),
399 Err(ObservationQualityParseError::Empty)
400 );
401 }
402
403 #[test]
404 fn custom_observation_kind() {
405 assert_eq!(
406 ObservationKind::from_str("pilot report").unwrap(),
407 ObservationKind::Custom(String::from("pilot report"))
408 );
409 }
410
411 #[test]
412 fn constructs_weather_observation_metadata() {
413 let observation = WeatherObservation::new(
414 WeatherObservationId::new("obs-002").unwrap(),
415 ObservationKind::Radar,
416 ObservationSource::new("regional radar composite").unwrap(),
417 ObservationQuality::Estimated,
418 );
419
420 assert_eq!(observation.id().as_str(), "obs-002");
421 assert_eq!(observation.kind(), &ObservationKind::Radar);
422 assert_eq!(observation.source().as_str(), "regional radar composite");
423 assert_eq!(observation.quality(), &ObservationQuality::Estimated);
424 }
425}