Skip to main content

use_place/
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, 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    /// Creates a place name from non-empty text.
56    ///
57    /// # Errors
58    ///
59    /// Returns [`PlaceTextError::Empty`] when the trimmed value is empty.
60    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    /// Creates a place identifier from non-empty text.
168    ///
169    /// # Errors
170    ///
171    /// Returns [`PlaceTextError::Empty`] when the trimmed value is empty.
172    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}