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, PlaceTextError> {
8 let trimmed = value.as_ref().trim();
9
10 if trimmed.is_empty() {
11 Err(PlaceTextError::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 PlaceTextError {
23 Empty,
24}
25
26impl fmt::Display for PlaceTextError {
27 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 Self::Empty => formatter.write_str("place text cannot be empty"),
30 }
31 }
32}
33
34impl Error for PlaceTextError {}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum PlaceKindParseError {
38 Empty,
39}
40
41impl fmt::Display for PlaceKindParseError {
42 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 Self::Empty => formatter.write_str("place kind cannot be empty"),
45 }
46 }
47}
48
49impl Error for PlaceKindParseError {}
50
51#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
52pub struct PlaceName(String);
53
54impl PlaceName {
55 pub fn new(value: impl AsRef<str>) -> Result<Self, PlaceTextError> {
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 PlaceName {
76 fn as_ref(&self) -> &str {
77 self.as_str()
78 }
79}
80
81impl fmt::Display for PlaceName {
82 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83 formatter.write_str(self.as_str())
84 }
85}
86
87impl FromStr for PlaceName {
88 type Err = PlaceTextError;
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 PlaceKind {
97 Continent,
98 Country,
99 Region,
100 State,
101 Province,
102 County,
103 City,
104 Town,
105 Village,
106 Neighborhood,
107 Landmark,
108 NaturalFeature,
109 Unknown,
110 Custom(String),
111}
112
113impl fmt::Display for PlaceKind {
114 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
115 match self {
116 Self::Continent => formatter.write_str("continent"),
117 Self::Country => formatter.write_str("country"),
118 Self::Region => formatter.write_str("region"),
119 Self::State => formatter.write_str("state"),
120 Self::Province => formatter.write_str("province"),
121 Self::County => formatter.write_str("county"),
122 Self::City => formatter.write_str("city"),
123 Self::Town => formatter.write_str("town"),
124 Self::Village => formatter.write_str("village"),
125 Self::Neighborhood => formatter.write_str("neighborhood"),
126 Self::Landmark => formatter.write_str("landmark"),
127 Self::NaturalFeature => formatter.write_str("natural-feature"),
128 Self::Unknown => formatter.write_str("unknown"),
129 Self::Custom(value) => formatter.write_str(value),
130 }
131 }
132}
133
134impl FromStr for PlaceKind {
135 type Err = PlaceKindParseError;
136
137 fn from_str(value: &str) -> Result<Self, Self::Err> {
138 let trimmed = value.trim();
139
140 if trimmed.is_empty() {
141 return Err(PlaceKindParseError::Empty);
142 }
143
144 Ok(match normalized_token(trimmed).as_str() {
145 "continent" => Self::Continent,
146 "country" => Self::Country,
147 "region" => Self::Region,
148 "state" => Self::State,
149 "province" => Self::Province,
150 "county" => Self::County,
151 "city" => Self::City,
152 "town" => Self::Town,
153 "village" => Self::Village,
154 "neighborhood" | "neighbourhood" => Self::Neighborhood,
155 "landmark" => Self::Landmark,
156 "natural-feature" => Self::NaturalFeature,
157 "unknown" => Self::Unknown,
158 _ => Self::Custom(trimmed.to_string()),
159 })
160 }
161}
162
163#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
164pub struct PlaceId(String);
165
166impl PlaceId {
167 pub fn new(value: impl AsRef<str>) -> Result<Self, PlaceTextError> {
173 non_empty_text(value).map(Self)
174 }
175
176 #[must_use]
177 pub fn as_str(&self) -> &str {
178 &self.0
179 }
180
181 #[must_use]
182 pub fn into_string(self) -> String {
183 self.0
184 }
185}
186
187impl AsRef<str> for PlaceId {
188 fn as_ref(&self) -> &str {
189 self.as_str()
190 }
191}
192
193impl fmt::Display for PlaceId {
194 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
195 formatter.write_str(self.as_str())
196 }
197}
198
199impl FromStr for PlaceId {
200 type Err = PlaceTextError;
201
202 fn from_str(value: &str) -> Result<Self, Self::Err> {
203 Self::new(value)
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::{PlaceId, PlaceKind, PlaceKindParseError, PlaceName, PlaceTextError};
210
211 #[test]
212 fn valid_place_name() -> Result<(), PlaceTextError> {
213 let place_name = PlaceName::new("Quito")?;
214
215 assert_eq!(place_name.as_str(), "Quito");
216 Ok(())
217 }
218
219 #[test]
220 fn empty_place_name_rejected() {
221 assert_eq!(PlaceName::new(" "), Err(PlaceTextError::Empty));
222 }
223
224 #[test]
225 fn place_kind_display_parse() -> Result<(), PlaceKindParseError> {
226 assert_eq!(PlaceKind::City.to_string(), "city");
227 assert_eq!(
228 "natural feature".parse::<PlaceKind>()?,
229 PlaceKind::NaturalFeature
230 );
231 Ok(())
232 }
233
234 #[test]
235 fn custom_place_kind() -> Result<(), PlaceKindParseError> {
236 assert_eq!(
237 "district".parse::<PlaceKind>()?,
238 PlaceKind::Custom(String::from("district"))
239 );
240 Ok(())
241 }
242
243 #[test]
244 fn place_id_construction() -> Result<(), PlaceTextError> {
245 let place_id = PlaceId::new("sf-ca-us")?;
246
247 assert_eq!(place_id.as_str(), "sf-ca-us");
248 Ok(())
249 }
250}