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 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 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}