Skip to main content

use_ecosystem/
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, EcosystemTextError> {
12    let trimmed = value.as_ref().trim();
13
14    if trimmed.is_empty() {
15        Err(EcosystemTextError::Empty)
16    } else {
17        Ok(trimmed.to_string())
18    }
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum EcosystemTextError {
23    Empty,
24}
25
26impl fmt::Display for EcosystemTextError {
27    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Self::Empty => formatter.write_str("ecosystem text cannot be empty"),
30        }
31    }
32}
33
34impl Error for EcosystemTextError {}
35
36#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
37pub struct EcosystemName(String);
38
39impl EcosystemName {
40    /// # Errors
41    /// Returns `EcosystemTextError::Empty` when `value` is blank.
42    pub fn new(value: impl AsRef<str>) -> Result<Self, EcosystemTextError> {
43        non_empty_text(value).map(Self)
44    }
45
46    #[must_use]
47    pub fn as_str(&self) -> &str {
48        &self.0
49    }
50
51    #[must_use]
52    pub fn into_string(self) -> String {
53        self.0
54    }
55}
56
57impl AsRef<str> for EcosystemName {
58    fn as_ref(&self) -> &str {
59        self.as_str()
60    }
61}
62
63impl fmt::Display for EcosystemName {
64    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
65        formatter.write_str(self.as_str())
66    }
67}
68
69impl FromStr for EcosystemName {
70    type Err = EcosystemTextError;
71
72    fn from_str(value: &str) -> Result<Self, Self::Err> {
73        Self::new(value)
74    }
75}
76
77#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
78pub struct EcosystemComponent(String);
79
80impl EcosystemComponent {
81    /// # Errors
82    /// Returns `EcosystemTextError::Empty` when `value` is blank.
83    pub fn new(value: impl AsRef<str>) -> Result<Self, EcosystemTextError> {
84        non_empty_text(value).map(Self)
85    }
86
87    #[must_use]
88    pub fn as_str(&self) -> &str {
89        &self.0
90    }
91}
92
93impl fmt::Display for EcosystemComponent {
94    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
95        formatter.write_str(self.as_str())
96    }
97}
98
99impl FromStr for EcosystemComponent {
100    type Err = EcosystemTextError;
101
102    fn from_str(value: &str) -> Result<Self, Self::Err> {
103        Self::new(value)
104    }
105}
106
107#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
108pub enum EcosystemKind {
109    Terrestrial,
110    Aquatic,
111    Marine,
112    Freshwater,
113    Forest,
114    Grassland,
115    Desert,
116    Tundra,
117    Wetland,
118    Urban,
119    Agricultural,
120    Unknown,
121    Custom(String),
122}
123
124impl fmt::Display for EcosystemKind {
125    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
126        formatter.write_str(match self {
127            Self::Terrestrial => "terrestrial",
128            Self::Aquatic => "aquatic",
129            Self::Marine => "marine",
130            Self::Freshwater => "freshwater",
131            Self::Forest => "forest",
132            Self::Grassland => "grassland",
133            Self::Desert => "desert",
134            Self::Tundra => "tundra",
135            Self::Wetland => "wetland",
136            Self::Urban => "urban",
137            Self::Agricultural => "agricultural",
138            Self::Unknown => "unknown",
139            Self::Custom(value) => value.as_str(),
140        })
141    }
142}
143
144impl FromStr for EcosystemKind {
145    type Err = EcosystemKindParseError;
146
147    fn from_str(value: &str) -> Result<Self, Self::Err> {
148        let trimmed = value.trim();
149
150        if trimmed.is_empty() {
151            return Err(EcosystemKindParseError::Empty);
152        }
153
154        Ok(match normalized_token(trimmed).as_str() {
155            "terrestrial" => Self::Terrestrial,
156            "aquatic" => Self::Aquatic,
157            "marine" => Self::Marine,
158            "freshwater" => Self::Freshwater,
159            "forest" => Self::Forest,
160            "grassland" => Self::Grassland,
161            "desert" => Self::Desert,
162            "tundra" => Self::Tundra,
163            "wetland" => Self::Wetland,
164            "urban" => Self::Urban,
165            "agricultural" => Self::Agricultural,
166            "unknown" => Self::Unknown,
167            _ => Self::Custom(trimmed.to_string()),
168        })
169    }
170}
171
172#[derive(Clone, Copy, Debug, Eq, PartialEq)]
173pub enum EcosystemKindParseError {
174    Empty,
175}
176
177impl fmt::Display for EcosystemKindParseError {
178    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
179        match self {
180            Self::Empty => formatter.write_str("ecosystem kind cannot be empty"),
181        }
182    }
183}
184
185impl Error for EcosystemKindParseError {}
186
187#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
188pub enum EcosystemScale {
189    Micro,
190    Local,
191    Regional,
192    Biome,
193    Global,
194    Unknown,
195    Custom(String),
196}
197
198impl fmt::Display for EcosystemScale {
199    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
200        formatter.write_str(match self {
201            Self::Micro => "micro",
202            Self::Local => "local",
203            Self::Regional => "regional",
204            Self::Biome => "biome",
205            Self::Global => "global",
206            Self::Unknown => "unknown",
207            Self::Custom(value) => value.as_str(),
208        })
209    }
210}
211
212impl FromStr for EcosystemScale {
213    type Err = EcosystemScaleParseError;
214
215    fn from_str(value: &str) -> Result<Self, Self::Err> {
216        let trimmed = value.trim();
217
218        if trimmed.is_empty() {
219            return Err(EcosystemScaleParseError::Empty);
220        }
221
222        Ok(match normalized_token(trimmed).as_str() {
223            "micro" => Self::Micro,
224            "local" => Self::Local,
225            "regional" => Self::Regional,
226            "biome" => Self::Biome,
227            "global" => Self::Global,
228            "unknown" => Self::Unknown,
229            _ => Self::Custom(trimmed.to_string()),
230        })
231    }
232}
233
234#[derive(Clone, Copy, Debug, Eq, PartialEq)]
235pub enum EcosystemScaleParseError {
236    Empty,
237}
238
239impl fmt::Display for EcosystemScaleParseError {
240    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
241        match self {
242            Self::Empty => formatter.write_str("ecosystem scale cannot be empty"),
243        }
244    }
245}
246
247impl Error for EcosystemScaleParseError {}
248
249#[cfg(test)]
250mod tests {
251    use super::{EcosystemKind, EcosystemName, EcosystemScale, EcosystemTextError};
252
253    #[test]
254    fn valid_ecosystem_name() -> Result<(), EcosystemTextError> {
255        let name = EcosystemName::new("temperate marsh")?;
256
257        assert_eq!(name.as_str(), "temperate marsh");
258        assert_eq!(name.to_string(), "temperate marsh");
259        Ok(())
260    }
261
262    #[test]
263    fn empty_ecosystem_name_rejected() {
264        assert_eq!(EcosystemName::new("  "), Err(EcosystemTextError::Empty));
265    }
266
267    #[test]
268    fn ecosystem_kind_display_parse() {
269        assert_eq!(
270            "wetland".parse::<EcosystemKind>(),
271            Ok(EcosystemKind::Wetland)
272        );
273        assert_eq!(EcosystemKind::Freshwater.to_string(), "freshwater");
274    }
275
276    #[test]
277    fn ecosystem_scale_display_parse() {
278        assert_eq!(
279            "regional".parse::<EcosystemScale>(),
280            Ok(EcosystemScale::Regional)
281        );
282        assert_eq!(EcosystemScale::Global.to_string(), "global");
283    }
284
285    #[test]
286    fn custom_ecosystem_kind() {
287        assert_eq!(
288            "kelp forest mosaic".parse::<EcosystemKind>(),
289            Ok(EcosystemKind::Custom("kelp forest mosaic".to_string()))
290        );
291    }
292}