Skip to main content

use_boundary/
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, BoundaryTextError> {
8    let trimmed = value.as_ref().trim();
9
10    if trimmed.is_empty() {
11        Err(BoundaryTextError::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 BoundaryTextError {
23    Empty,
24}
25
26impl fmt::Display for BoundaryTextError {
27    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Self::Empty => formatter.write_str("boundary text cannot be empty"),
30        }
31    }
32}
33
34impl Error for BoundaryTextError {}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum BoundaryParseError {
38    Empty,
39}
40
41impl fmt::Display for BoundaryParseError {
42    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43        match self {
44            Self::Empty => formatter.write_str("boundary vocabulary cannot be empty"),
45        }
46    }
47}
48
49impl Error for BoundaryParseError {}
50
51#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
52pub struct BoundaryName(String);
53
54impl BoundaryName {
55    /// Creates a boundary name from non-empty text.
56    ///
57    /// # Errors
58    ///
59    /// Returns [`BoundaryTextError::Empty`] when the trimmed value is empty.
60    pub fn new(value: impl AsRef<str>) -> Result<Self, BoundaryTextError> {
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 BoundaryName {
76    fn as_ref(&self) -> &str {
77        self.as_str()
78    }
79}
80
81impl fmt::Display for BoundaryName {
82    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83        formatter.write_str(self.as_str())
84    }
85}
86
87impl FromStr for BoundaryName {
88    type Err = BoundaryTextError;
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 BoundaryKind {
97    Political,
98    Administrative,
99    Natural,
100    Coastline,
101    River,
102    Watershed,
103    Property,
104    ProtectedArea,
105    Maritime,
106    Unknown,
107    Custom(String),
108}
109
110impl fmt::Display for BoundaryKind {
111    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
112        match self {
113            Self::Political => formatter.write_str("political"),
114            Self::Administrative => formatter.write_str("administrative"),
115            Self::Natural => formatter.write_str("natural"),
116            Self::Coastline => formatter.write_str("coastline"),
117            Self::River => formatter.write_str("river"),
118            Self::Watershed => formatter.write_str("watershed"),
119            Self::Property => formatter.write_str("property"),
120            Self::ProtectedArea => formatter.write_str("protected-area"),
121            Self::Maritime => formatter.write_str("maritime"),
122            Self::Unknown => formatter.write_str("unknown"),
123            Self::Custom(value) => formatter.write_str(value),
124        }
125    }
126}
127
128impl FromStr for BoundaryKind {
129    type Err = BoundaryParseError;
130
131    fn from_str(value: &str) -> Result<Self, Self::Err> {
132        let trimmed = value.trim();
133
134        if trimmed.is_empty() {
135            return Err(BoundaryParseError::Empty);
136        }
137
138        Ok(match normalized_token(trimmed).as_str() {
139            "political" => Self::Political,
140            "administrative" => Self::Administrative,
141            "natural" => Self::Natural,
142            "coastline" => Self::Coastline,
143            "river" => Self::River,
144            "watershed" => Self::Watershed,
145            "property" => Self::Property,
146            "protected-area" => Self::ProtectedArea,
147            "maritime" => Self::Maritime,
148            "unknown" => Self::Unknown,
149            _ => Self::Custom(trimmed.to_string()),
150        })
151    }
152}
153
154#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
155pub enum BoundaryStatus {
156    Official,
157    Disputed,
158    Approximate,
159    Historical,
160    Unknown,
161    Custom(String),
162}
163
164impl fmt::Display for BoundaryStatus {
165    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
166        match self {
167            Self::Official => formatter.write_str("official"),
168            Self::Disputed => formatter.write_str("disputed"),
169            Self::Approximate => formatter.write_str("approximate"),
170            Self::Historical => formatter.write_str("historical"),
171            Self::Unknown => formatter.write_str("unknown"),
172            Self::Custom(value) => formatter.write_str(value),
173        }
174    }
175}
176
177impl FromStr for BoundaryStatus {
178    type Err = BoundaryParseError;
179
180    fn from_str(value: &str) -> Result<Self, Self::Err> {
181        let trimmed = value.trim();
182
183        if trimmed.is_empty() {
184            return Err(BoundaryParseError::Empty);
185        }
186
187        Ok(match normalized_token(trimmed).as_str() {
188            "official" => Self::Official,
189            "disputed" => Self::Disputed,
190            "approximate" => Self::Approximate,
191            "historical" => Self::Historical,
192            "unknown" => Self::Unknown,
193            _ => Self::Custom(trimmed.to_string()),
194        })
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::{
201        BoundaryKind, BoundaryName, BoundaryParseError, BoundaryStatus, BoundaryTextError,
202    };
203
204    #[test]
205    fn valid_boundary_name() -> Result<(), BoundaryTextError> {
206        let boundary_name = BoundaryName::new("Arctic Circle")?;
207
208        assert_eq!(boundary_name.as_str(), "Arctic Circle");
209        Ok(())
210    }
211
212    #[test]
213    fn empty_boundary_name_rejected() {
214        assert_eq!(BoundaryName::new("  "), Err(BoundaryTextError::Empty));
215    }
216
217    #[test]
218    fn boundary_kind_display_parse() -> Result<(), BoundaryParseError> {
219        assert_eq!(BoundaryKind::Political.to_string(), "political");
220        assert_eq!(
221            "protected area".parse::<BoundaryKind>()?,
222            BoundaryKind::ProtectedArea
223        );
224        Ok(())
225    }
226
227    #[test]
228    fn boundary_status_display_parse() -> Result<(), BoundaryParseError> {
229        assert_eq!(BoundaryStatus::Historical.to_string(), "historical");
230        assert_eq!(
231            "disputed".parse::<BoundaryStatus>()?,
232            BoundaryStatus::Disputed
233        );
234        Ok(())
235    }
236
237    #[test]
238    fn custom_boundary_kind() -> Result<(), BoundaryParseError> {
239        assert_eq!(
240            "tribal".parse::<BoundaryKind>()?,
241            BoundaryKind::Custom(String::from("tribal"))
242        );
243        Ok(())
244    }
245}