Skip to main content

use_conservation_status/
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_token(value: &str) -> String {
8    value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
9}
10
11fn non_empty_text(value: impl AsRef<str>) -> Result<String, ConservationTextError> {
12    let trimmed = value.as_ref().trim();
13
14    if trimmed.is_empty() {
15        Err(ConservationTextError::Empty)
16    } else {
17        Ok(trimmed.to_string())
18    }
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum ConservationTextError {
23    Empty,
24}
25
26impl fmt::Display for ConservationTextError {
27    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Self::Empty => formatter.write_str("conservation text cannot be empty"),
30        }
31    }
32}
33
34impl Error for ConservationTextError {}
35
36#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
37pub enum ConservationStatus {
38    LeastConcern,
39    NearThreatened,
40    Vulnerable,
41    Endangered,
42    CriticallyEndangered,
43    ExtinctInTheWild,
44    Extinct,
45    DataDeficient,
46    NotEvaluated,
47    Unknown,
48    Custom(String),
49}
50
51impl fmt::Display for ConservationStatus {
52    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53        formatter.write_str(match self {
54            Self::LeastConcern => "least-concern",
55            Self::NearThreatened => "near-threatened",
56            Self::Vulnerable => "vulnerable",
57            Self::Endangered => "endangered",
58            Self::CriticallyEndangered => "critically-endangered",
59            Self::ExtinctInTheWild => "extinct-in-the-wild",
60            Self::Extinct => "extinct",
61            Self::DataDeficient => "data-deficient",
62            Self::NotEvaluated => "not-evaluated",
63            Self::Unknown => "unknown",
64            Self::Custom(value) => value.as_str(),
65        })
66    }
67}
68
69impl FromStr for ConservationStatus {
70    type Err = ConservationStatusParseError;
71
72    fn from_str(value: &str) -> Result<Self, Self::Err> {
73        let trimmed = value.trim();
74
75        if trimmed.is_empty() {
76            return Err(ConservationStatusParseError::Empty);
77        }
78
79        Ok(match normalized_token(trimmed).as_str() {
80            "least-concern" => Self::LeastConcern,
81            "near-threatened" => Self::NearThreatened,
82            "vulnerable" => Self::Vulnerable,
83            "endangered" => Self::Endangered,
84            "critically-endangered" => Self::CriticallyEndangered,
85            "extinct-in-the-wild" => Self::ExtinctInTheWild,
86            "extinct" => Self::Extinct,
87            "data-deficient" => Self::DataDeficient,
88            "not-evaluated" => Self::NotEvaluated,
89            "unknown" => Self::Unknown,
90            _ => Self::Custom(trimmed.to_string()),
91        })
92    }
93}
94
95#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96pub enum ConservationStatusParseError {
97    Empty,
98}
99
100impl fmt::Display for ConservationStatusParseError {
101    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
102        match self {
103            Self::Empty => formatter.write_str("conservation status cannot be empty"),
104        }
105    }
106}
107
108impl Error for ConservationStatusParseError {}
109
110#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
111pub enum ThreatKind {
112    HabitatLoss,
113    ClimateChange,
114    Pollution,
115    InvasiveSpecies,
116    Overexploitation,
117    Disease,
118    Fragmentation,
119    Unknown,
120    Custom(String),
121}
122
123impl fmt::Display for ThreatKind {
124    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125        formatter.write_str(match self {
126            Self::HabitatLoss => "habitat-loss",
127            Self::ClimateChange => "climate-change",
128            Self::Pollution => "pollution",
129            Self::InvasiveSpecies => "invasive-species",
130            Self::Overexploitation => "overexploitation",
131            Self::Disease => "disease",
132            Self::Fragmentation => "fragmentation",
133            Self::Unknown => "unknown",
134            Self::Custom(value) => value.as_str(),
135        })
136    }
137}
138
139impl FromStr for ThreatKind {
140    type Err = ThreatKindParseError;
141
142    fn from_str(value: &str) -> Result<Self, Self::Err> {
143        let trimmed = value.trim();
144
145        if trimmed.is_empty() {
146            return Err(ThreatKindParseError::Empty);
147        }
148
149        Ok(match normalized_token(trimmed).as_str() {
150            "habitat-loss" => Self::HabitatLoss,
151            "climate-change" => Self::ClimateChange,
152            "pollution" => Self::Pollution,
153            "invasive-species" => Self::InvasiveSpecies,
154            "overexploitation" => Self::Overexploitation,
155            "disease" => Self::Disease,
156            "fragmentation" => Self::Fragmentation,
157            "unknown" => Self::Unknown,
158            _ => Self::Custom(trimmed.to_string()),
159        })
160    }
161}
162
163#[derive(Clone, Copy, Debug, Eq, PartialEq)]
164pub enum ThreatKindParseError {
165    Empty,
166}
167
168impl fmt::Display for ThreatKindParseError {
169    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
170        match self {
171            Self::Empty => formatter.write_str("threat kind cannot be empty"),
172        }
173    }
174}
175
176impl Error for ThreatKindParseError {}
177
178#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
179pub struct ConservationStatusSystem(String);
180
181impl ConservationStatusSystem {
182    /// # Errors
183    /// Returns `ConservationTextError::Empty` when `value` is blank.
184    pub fn new(value: impl AsRef<str>) -> Result<Self, ConservationTextError> {
185        non_empty_text(value).map(Self)
186    }
187
188    #[must_use]
189    pub fn as_str(&self) -> &str {
190        &self.0
191    }
192}
193
194impl fmt::Display for ConservationStatusSystem {
195    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
196        formatter.write_str(self.as_str())
197    }
198}
199
200#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
201pub struct ProtectionStatus(String);
202
203impl ProtectionStatus {
204    /// # Errors
205    /// Returns `ConservationTextError::Empty` when `value` is blank.
206    pub fn new(value: impl AsRef<str>) -> Result<Self, ConservationTextError> {
207        non_empty_text(value).map(Self)
208    }
209
210    #[must_use]
211    pub fn as_str(&self) -> &str {
212        &self.0
213    }
214}
215
216impl fmt::Display for ProtectionStatus {
217    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
218        formatter.write_str(self.as_str())
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::{
225        ConservationStatus, ConservationStatusSystem, ConservationTextError, ProtectionStatus,
226        ThreatKind,
227    };
228
229    #[test]
230    fn conservation_status_display_parse() {
231        assert_eq!(
232            "vulnerable".parse::<ConservationStatus>(),
233            Ok(ConservationStatus::Vulnerable)
234        );
235        assert_eq!(ConservationStatus::Endangered.to_string(), "endangered");
236    }
237
238    #[test]
239    fn custom_conservation_status() {
240        assert_eq!(
241            "regionally-endemic".parse::<ConservationStatus>(),
242            Ok(ConservationStatus::Custom("regionally-endemic".to_string()))
243        );
244    }
245
246    #[test]
247    fn threat_kind_display_parse() {
248        assert_eq!(
249            "habitat-loss".parse::<ThreatKind>(),
250            Ok(ThreatKind::HabitatLoss)
251        );
252        assert_eq!(ThreatKind::ClimateChange.to_string(), "climate-change");
253    }
254
255    #[test]
256    fn protection_status_construction() -> Result<(), ConservationTextError> {
257        let protection = ProtectionStatus::new("protected-area")?;
258
259        assert_eq!(protection.to_string(), "protected-area");
260        Ok(())
261    }
262
263    #[test]
264    fn conservation_status_system_construction() -> Result<(), ConservationTextError> {
265        let system = ConservationStatusSystem::new("IUCN")?;
266
267        assert_eq!(system.to_string(), "IUCN");
268        Ok(())
269    }
270}