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