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