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