Skip to main content

use_habitat/
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, HabitatTextError> {
12    let trimmed = value.as_ref().trim();
13
14    if trimmed.is_empty() {
15        Err(HabitatTextError::Empty)
16    } else {
17        Ok(trimmed.to_string())
18    }
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum HabitatTextError {
23    Empty,
24}
25
26impl fmt::Display for HabitatTextError {
27    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Self::Empty => formatter.write_str("habitat text cannot be empty"),
30        }
31    }
32}
33
34impl Error for HabitatTextError {}
35
36#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
37pub struct HabitatName(String);
38
39impl HabitatName {
40    /// # Errors
41    /// Returns `HabitatTextError::Empty` when `value` is blank.
42    pub fn new(value: impl AsRef<str>) -> Result<Self, HabitatTextError> {
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
52impl fmt::Display for HabitatName {
53    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54        formatter.write_str(self.as_str())
55    }
56}
57
58impl FromStr for HabitatName {
59    type Err = HabitatTextError;
60
61    fn from_str(value: &str) -> Result<Self, Self::Err> {
62        Self::new(value)
63    }
64}
65
66#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
67pub struct HabitatFeature(String);
68
69impl HabitatFeature {
70    /// # Errors
71    /// Returns `HabitatTextError::Empty` when `value` is blank.
72    pub fn new(value: impl AsRef<str>) -> Result<Self, HabitatTextError> {
73        non_empty_text(value).map(Self)
74    }
75
76    #[must_use]
77    pub fn as_str(&self) -> &str {
78        &self.0
79    }
80}
81
82impl fmt::Display for HabitatFeature {
83    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84        formatter.write_str(self.as_str())
85    }
86}
87
88impl FromStr for HabitatFeature {
89    type Err = HabitatTextError;
90
91    fn from_str(value: &str) -> Result<Self, Self::Err> {
92        Self::new(value)
93    }
94}
95
96#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
97pub enum HabitatKind {
98    Forest,
99    Grassland,
100    Desert,
101    Wetland,
102    River,
103    Lake,
104    Ocean,
105    Reef,
106    Cave,
107    Soil,
108    Urban,
109    Agricultural,
110    Unknown,
111    Custom(String),
112}
113
114impl fmt::Display for HabitatKind {
115    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
116        formatter.write_str(match self {
117            Self::Forest => "forest",
118            Self::Grassland => "grassland",
119            Self::Desert => "desert",
120            Self::Wetland => "wetland",
121            Self::River => "river",
122            Self::Lake => "lake",
123            Self::Ocean => "ocean",
124            Self::Reef => "reef",
125            Self::Cave => "cave",
126            Self::Soil => "soil",
127            Self::Urban => "urban",
128            Self::Agricultural => "agricultural",
129            Self::Unknown => "unknown",
130            Self::Custom(value) => value.as_str(),
131        })
132    }
133}
134
135impl FromStr for HabitatKind {
136    type Err = HabitatKindParseError;
137
138    fn from_str(value: &str) -> Result<Self, Self::Err> {
139        let trimmed = value.trim();
140
141        if trimmed.is_empty() {
142            return Err(HabitatKindParseError::Empty);
143        }
144
145        Ok(match normalized_token(trimmed).as_str() {
146            "forest" => Self::Forest,
147            "grassland" => Self::Grassland,
148            "desert" => Self::Desert,
149            "wetland" => Self::Wetland,
150            "river" => Self::River,
151            "lake" => Self::Lake,
152            "ocean" => Self::Ocean,
153            "reef" => Self::Reef,
154            "cave" => Self::Cave,
155            "soil" => Self::Soil,
156            "urban" => Self::Urban,
157            "agricultural" => Self::Agricultural,
158            "unknown" => Self::Unknown,
159            _ => Self::Custom(trimmed.to_string()),
160        })
161    }
162}
163
164#[derive(Clone, Copy, Debug, Eq, PartialEq)]
165pub enum HabitatKindParseError {
166    Empty,
167}
168
169impl fmt::Display for HabitatKindParseError {
170    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
171        match self {
172            Self::Empty => formatter.write_str("habitat kind cannot be empty"),
173        }
174    }
175}
176
177impl Error for HabitatKindParseError {}
178
179#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
180pub enum HabitatCondition {
181    Intact,
182    Fragmented,
183    Degraded,
184    Restored,
185    Protected,
186    Disturbed,
187    Unknown,
188    Custom(String),
189}
190
191impl fmt::Display for HabitatCondition {
192    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
193        formatter.write_str(match self {
194            Self::Intact => "intact",
195            Self::Fragmented => "fragmented",
196            Self::Degraded => "degraded",
197            Self::Restored => "restored",
198            Self::Protected => "protected",
199            Self::Disturbed => "disturbed",
200            Self::Unknown => "unknown",
201            Self::Custom(value) => value.as_str(),
202        })
203    }
204}
205
206impl FromStr for HabitatCondition {
207    type Err = HabitatConditionParseError;
208
209    fn from_str(value: &str) -> Result<Self, Self::Err> {
210        let trimmed = value.trim();
211
212        if trimmed.is_empty() {
213            return Err(HabitatConditionParseError::Empty);
214        }
215
216        Ok(match normalized_token(trimmed).as_str() {
217            "intact" => Self::Intact,
218            "fragmented" => Self::Fragmented,
219            "degraded" => Self::Degraded,
220            "restored" => Self::Restored,
221            "protected" => Self::Protected,
222            "disturbed" => Self::Disturbed,
223            "unknown" => Self::Unknown,
224            _ => Self::Custom(trimmed.to_string()),
225        })
226    }
227}
228
229#[derive(Clone, Copy, Debug, Eq, PartialEq)]
230pub enum HabitatConditionParseError {
231    Empty,
232}
233
234impl fmt::Display for HabitatConditionParseError {
235    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
236        match self {
237            Self::Empty => formatter.write_str("habitat condition cannot be empty"),
238        }
239    }
240}
241
242impl Error for HabitatConditionParseError {}
243
244#[cfg(test)]
245mod tests {
246    use super::{HabitatCondition, HabitatKind, HabitatName, HabitatTextError};
247
248    #[test]
249    fn valid_habitat_name() -> Result<(), HabitatTextError> {
250        let name = HabitatName::new("riparian corridor")?;
251
252        assert_eq!(name.as_str(), "riparian corridor");
253        Ok(())
254    }
255
256    #[test]
257    fn empty_habitat_name_rejected() {
258        assert_eq!(HabitatName::new(" "), Err(HabitatTextError::Empty));
259    }
260
261    #[test]
262    fn habitat_kind_display_parse() {
263        assert_eq!("reef".parse::<HabitatKind>(), Ok(HabitatKind::Reef));
264        assert_eq!(HabitatKind::Agricultural.to_string(), "agricultural");
265    }
266
267    #[test]
268    fn habitat_condition_display_parse() {
269        assert_eq!(
270            "protected".parse::<HabitatCondition>(),
271            Ok(HabitatCondition::Protected)
272        );
273        assert_eq!(HabitatCondition::Fragmented.to_string(), "fragmented");
274    }
275
276    #[test]
277    fn custom_habitat_kind() {
278        assert_eq!(
279            "tidal-flat".parse::<HabitatKind>(),
280            Ok(HabitatKind::Custom("tidal-flat".to_string()))
281        );
282    }
283}