Skip to main content

use_geographic_region/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn non_empty_text(value: impl AsRef<str>) -> Result<String, GeographicRegionTextError> {
8    let trimmed = value.as_ref().trim();
9
10    if trimmed.is_empty() {
11        Err(GeographicRegionTextError::Empty)
12    } else {
13        Ok(trimmed.to_string())
14    }
15}
16
17fn normalized_token(value: &str) -> String {
18    value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum GeographicRegionTextError {
23    Empty,
24}
25
26impl fmt::Display for GeographicRegionTextError {
27    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Self::Empty => formatter.write_str("geographic region text cannot be empty"),
30        }
31    }
32}
33
34impl Error for GeographicRegionTextError {}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum GeographicRegionKindParseError {
38    Empty,
39}
40
41impl fmt::Display for GeographicRegionKindParseError {
42    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43        match self {
44            Self::Empty => formatter.write_str("geographic region kind cannot be empty"),
45        }
46    }
47}
48
49impl Error for GeographicRegionKindParseError {}
50
51#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
52pub struct GeographicRegionName(String);
53
54impl GeographicRegionName {
55    /// Creates a geographic region name from non-empty text.
56    ///
57    /// # Errors
58    ///
59    /// Returns [`GeographicRegionTextError::Empty`] when the trimmed value is empty.
60    pub fn new(value: impl AsRef<str>) -> Result<Self, GeographicRegionTextError> {
61        non_empty_text(value).map(Self)
62    }
63
64    #[must_use]
65    pub fn as_str(&self) -> &str {
66        &self.0
67    }
68
69    #[must_use]
70    pub fn into_string(self) -> String {
71        self.0
72    }
73}
74
75impl AsRef<str> for GeographicRegionName {
76    fn as_ref(&self) -> &str {
77        self.as_str()
78    }
79}
80
81impl fmt::Display for GeographicRegionName {
82    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83        formatter.write_str(self.as_str())
84    }
85}
86
87impl FromStr for GeographicRegionName {
88    type Err = GeographicRegionTextError;
89
90    fn from_str(value: &str) -> Result<Self, Self::Err> {
91        Self::new(value)
92    }
93}
94
95#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
96pub enum GeographicRegionKind {
97    Continent,
98    Country,
99    Administrative,
100    Political,
101    Cultural,
102    Natural,
103    Climate,
104    Economic,
105    Watershed,
106    Biome,
107    Unknown,
108    Custom(String),
109}
110
111impl fmt::Display for GeographicRegionKind {
112    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
113        match self {
114            Self::Continent => formatter.write_str("continent"),
115            Self::Country => formatter.write_str("country"),
116            Self::Administrative => formatter.write_str("administrative"),
117            Self::Political => formatter.write_str("political"),
118            Self::Cultural => formatter.write_str("cultural"),
119            Self::Natural => formatter.write_str("natural"),
120            Self::Climate => formatter.write_str("climate"),
121            Self::Economic => formatter.write_str("economic"),
122            Self::Watershed => formatter.write_str("watershed"),
123            Self::Biome => formatter.write_str("biome"),
124            Self::Unknown => formatter.write_str("unknown"),
125            Self::Custom(value) => formatter.write_str(value),
126        }
127    }
128}
129
130impl FromStr for GeographicRegionKind {
131    type Err = GeographicRegionKindParseError;
132
133    fn from_str(value: &str) -> Result<Self, Self::Err> {
134        let trimmed = value.trim();
135
136        if trimmed.is_empty() {
137            return Err(GeographicRegionKindParseError::Empty);
138        }
139
140        Ok(match normalized_token(trimmed).as_str() {
141            "continent" => Self::Continent,
142            "country" => Self::Country,
143            "administrative" => Self::Administrative,
144            "political" => Self::Political,
145            "cultural" => Self::Cultural,
146            "natural" => Self::Natural,
147            "climate" => Self::Climate,
148            "economic" => Self::Economic,
149            "watershed" => Self::Watershed,
150            "biome" => Self::Biome,
151            "unknown" => Self::Unknown,
152            _ => Self::Custom(trimmed.to_string()),
153        })
154    }
155}
156
157#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
158pub struct GeographicRegionId(String);
159
160impl GeographicRegionId {
161    /// Creates a geographic region identifier from non-empty text.
162    ///
163    /// # Errors
164    ///
165    /// Returns [`GeographicRegionTextError::Empty`] when the trimmed value is empty.
166    pub fn new(value: impl AsRef<str>) -> Result<Self, GeographicRegionTextError> {
167        non_empty_text(value).map(Self)
168    }
169
170    #[must_use]
171    pub fn as_str(&self) -> &str {
172        &self.0
173    }
174
175    #[must_use]
176    pub fn into_string(self) -> String {
177        self.0
178    }
179}
180
181impl AsRef<str> for GeographicRegionId {
182    fn as_ref(&self) -> &str {
183        self.as_str()
184    }
185}
186
187impl fmt::Display for GeographicRegionId {
188    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
189        formatter.write_str(self.as_str())
190    }
191}
192
193impl FromStr for GeographicRegionId {
194    type Err = GeographicRegionTextError;
195
196    fn from_str(value: &str) -> Result<Self, Self::Err> {
197        Self::new(value)
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::{
204        GeographicRegionId, GeographicRegionKind, GeographicRegionKindParseError,
205        GeographicRegionName, GeographicRegionTextError,
206    };
207
208    #[test]
209    fn valid_region_name() -> Result<(), GeographicRegionTextError> {
210        let region_name = GeographicRegionName::new("Andean Highlands")?;
211
212        assert_eq!(region_name.as_str(), "Andean Highlands");
213        Ok(())
214    }
215
216    #[test]
217    fn empty_region_name_rejected() {
218        assert_eq!(
219            GeographicRegionName::new("   "),
220            Err(GeographicRegionTextError::Empty)
221        );
222    }
223
224    #[test]
225    fn region_kind_display_parse() -> Result<(), GeographicRegionKindParseError> {
226        assert_eq!(GeographicRegionKind::Watershed.to_string(), "watershed");
227        assert_eq!(
228            "administrative".parse::<GeographicRegionKind>()?,
229            GeographicRegionKind::Administrative
230        );
231        Ok(())
232    }
233
234    #[test]
235    fn custom_region_kind() -> Result<(), GeographicRegionKindParseError> {
236        assert_eq!(
237            "ecoregion".parse::<GeographicRegionKind>()?,
238            GeographicRegionKind::Custom(String::from("ecoregion"))
239        );
240        Ok(())
241    }
242
243    #[test]
244    fn region_id_construction() -> Result<(), GeographicRegionTextError> {
245        let region_id = GeographicRegionId::new("andean-highlands")?;
246
247        assert_eq!(region_id.as_str(), "andean-highlands");
248        Ok(())
249    }
250}