1use std::fmt;
10use std::str::FromStr;
11
12use serde::{Deserialize, Serialize};
13
14use crate::error::{Error, Result};
15use crate::models::GeoCatalog;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub enum GeoLevel {
20 Region,
22 Subregion,
24 Country,
26}
27
28impl GeoLevel {
29 pub fn as_wire(self) -> &'static str {
31 match self {
32 Self::Region => "Region",
33 Self::Subregion => "Subregion",
34 Self::Country => "Country",
35 }
36 }
37}
38
39impl fmt::Display for GeoLevel {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 f.write_str(self.as_wire())
42 }
43}
44
45impl FromStr for GeoLevel {
46 type Err = Error;
47
48 fn from_str(text: &str) -> Result<Self> {
49 match text.trim().to_ascii_lowercase().as_str() {
50 "region" => Ok(Self::Region),
51 "subregion" | "sub-region" => Ok(Self::Subregion),
52 "country" => Ok(Self::Country),
53 _ => Err(Error::InvalidInput(format!(
54 "unknown geo level {text:?} (Region, Subregion or Country)"
55 ))),
56 }
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct GeoArea {
66 pub level: GeoLevel,
68 pub code: u16,
70}
71
72impl fmt::Display for GeoArea {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 write!(f, "{} {}", self.level, self.code)
75 }
76}
77
78#[derive(Debug, Clone, Serialize)]
80pub struct KnownArea<'a> {
81 pub level: GeoLevel,
83 pub code: u16,
85 pub name: &'a str,
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub alpha2: Option<&'a str>,
90}
91
92impl GeoCatalog {
93 pub fn parse_area(&self, input: &str) -> Result<GeoArea> {
126 let input = input.trim();
127 if let Some((level, rest)) = input.split_once(':') {
128 let level: GeoLevel = level.parse()?;
129 let rest = rest.trim();
130 if let Ok(code) = rest.parse::<u16>() {
131 let area = GeoArea { level, code };
132 return match self.name(area) {
133 Some(_) => Ok(area),
134 None => Err(Error::InvalidInput(format!(
135 "unknown {level} code {code} (see the list of known areas)"
136 ))),
137 };
138 }
139 return self
140 .find(rest, Some(level))
141 .ok_or_else(|| Error::InvalidInput(format!("unknown {level} {rest:?}")));
142 }
143 if input.parse::<u16>().is_ok() {
144 return Err(Error::InvalidInput(format!(
145 "{input:?} is ambiguous; write country:{input}, region:{input} or subregion:{input}"
146 )));
147 }
148 self.find(input, None).ok_or_else(|| {
149 Error::InvalidInput(format!(
150 "unknown area {input:?} (a country code like DE, a country, region or sub-region name, or level:code)"
151 ))
152 })
153 }
154
155 pub fn name(&self, area: GeoArea) -> Option<&str> {
157 match area.level {
158 GeoLevel::Country => self
159 .countries
160 .iter()
161 .find(|c| c.code == area.code)
162 .map(|c| c.name.as_str()),
163 GeoLevel::Region => self
164 .regions
165 .iter()
166 .find(|r| r.code == area.code)
167 .map(|r| r.name.as_str()),
168 GeoLevel::Subregion => self
169 .subregions
170 .iter()
171 .find(|s| s.code == area.code)
172 .map(|s| s.name.as_str()),
173 }
174 }
175
176 pub fn describe(&self, area: GeoArea) -> String {
179 match self.name(area) {
180 Some(name) => format!("{} {name} ({})", area.level, area.code),
181 None => area.to_string(),
182 }
183 }
184
185 pub fn known_areas(&self) -> Vec<KnownArea<'_>> {
187 let regions = self.regions.iter().map(|r| KnownArea {
188 level: GeoLevel::Region,
189 code: r.code,
190 name: &r.name,
191 alpha2: None,
192 });
193 let subregions = self.subregions.iter().map(|s| KnownArea {
194 level: GeoLevel::Subregion,
195 code: s.code,
196 name: &s.name,
197 alpha2: None,
198 });
199 let countries = self.countries.iter().map(|c| KnownArea {
200 level: GeoLevel::Country,
201 code: c.code,
202 name: &c.name,
203 alpha2: Some(&c.alpha2),
204 });
205 regions.chain(subregions).chain(countries).collect()
206 }
207
208 fn find(&self, text: &str, only: Option<GeoLevel>) -> Option<GeoArea> {
209 let wanted = |level| only.is_none_or(|l| l == level);
210 let area = |level, code| GeoArea { level, code };
211
212 if wanted(GeoLevel::Country)
213 && let Some(c) = self.countries.iter().find(|c| {
214 c.alpha2.eq_ignore_ascii_case(text)
215 || c.alpha3.eq_ignore_ascii_case(text)
216 || c.name.eq_ignore_ascii_case(text)
217 })
218 {
219 return Some(area(GeoLevel::Country, c.code));
220 }
221 if wanted(GeoLevel::Region)
222 && let Some(r) = self
223 .regions
224 .iter()
225 .find(|r| r.name.eq_ignore_ascii_case(text))
226 {
227 return Some(area(GeoLevel::Region, r.code));
228 }
229 if wanted(GeoLevel::Subregion)
230 && let Some(s) = self
231 .subregions
232 .iter()
233 .find(|s| s.name.eq_ignore_ascii_case(text))
234 {
235 return Some(area(GeoLevel::Subregion, s.code));
236 }
237 None
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 fn catalog() -> GeoCatalog {
247 serde_json::from_value(serde_json::json!({
248 "regions": [
249 { "code": 150, "name": "Europe" },
250 { "code": 19, "name": "Americas" }
251 ],
252 "subregions": [
253 { "code": 39, "name": "Southern Europe", "region_code": 150 },
254 { "code": 155, "name": "Western Europe", "region_code": 150 }
255 ],
256 "countries": [
257 { "code": 191, "alpha2": "HR", "alpha3": "HRV", "name": "Croatia",
258 "region_code": 150, "subregion_code": 39 },
259 { "code": 276, "alpha2": "DE", "alpha3": "DEU", "name": "Germany",
260 "region_code": 150, "subregion_code": 155 },
261 { "code": 10, "alpha2": "AQ", "alpha3": "ATA", "name": "Antarctica",
262 "region_code": null, "subregion_code": null }
263 ]
264 }))
265 .unwrap()
266 }
267
268 fn area(level: GeoLevel, code: u16) -> GeoArea {
269 GeoArea { level, code }
270 }
271
272 #[test]
273 fn country_without_region_deserializes() {
274 let aq = &catalog().countries[2];
275 assert_eq!((aq.region_code, aq.subregion_code), (None, None));
276 }
277
278 #[test]
279 fn countries() {
280 let c = catalog();
281 let de = area(GeoLevel::Country, 276);
282 assert_eq!(c.parse_area("DE").unwrap(), de);
283 assert_eq!(c.parse_area("deu").unwrap(), de);
284 assert_eq!(c.parse_area("germany").unwrap(), de);
285 assert_eq!(c.parse_area("country:276").unwrap(), de);
286 assert_eq!(c.parse_area("Country:DE").unwrap(), de);
287 assert_eq!(c.parse_area("HR").unwrap(), area(GeoLevel::Country, 191));
288 }
289
290 #[test]
291 fn regions() {
292 let c = catalog();
293 assert_eq!(c.parse_area("Europe").unwrap(), area(GeoLevel::Region, 150));
294 assert_eq!(
295 c.parse_area("southern europe").unwrap(),
296 area(GeoLevel::Subregion, 39)
297 );
298 assert_eq!(
299 c.parse_area("subregion:39").unwrap(),
300 area(GeoLevel::Subregion, 39)
301 );
302 }
303
304 #[test]
305 fn rejects_ambiguous_and_unknown() {
306 let c = catalog();
307 assert!(c.parse_area("150").is_err());
308 assert!(c.parse_area("Atlantis").is_err());
309 assert!(c.parse_area("region:Germany").is_err());
310 assert!(c.parse_area("planet:3").is_err());
311 assert!(c.parse_area("country:999").is_err());
312 assert!(c.parse_area("region:191").is_err());
313 }
314
315 #[test]
316 fn describe() {
317 let c = catalog();
318 assert_eq!(
319 c.describe(area(GeoLevel::Country, 191)),
320 "Country Croatia (191)"
321 );
322 assert_eq!(
323 c.describe(area(GeoLevel::Region, 150)),
324 "Region Europe (150)"
325 );
326 assert_eq!(c.describe(area(GeoLevel::Country, 999)), "Country 999");
327 }
328
329 #[test]
330 fn known_areas_in_order() {
331 let c = catalog();
332 let levels: Vec<GeoLevel> = c.known_areas().iter().map(|a| a.level).collect();
333 assert_eq!(levels.len(), 7);
334 assert_eq!(levels[0], GeoLevel::Region);
335 assert_eq!(levels[2], GeoLevel::Subregion);
336 assert_eq!(levels[6], GeoLevel::Country);
337 }
338}