use crate::prelude::*;
use crate::util::{Constant, Searchable};
use bon::Builder;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
pub trait GeonamesParser {
fn country_codes(format: CodeFormat) -> Vec<String>;
fn country_data() -> Countries;
fn languages(format: CodeFormat) -> Vec<String>;
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(transparent)]
pub struct Countries(pub Vec<Country>);
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub enum CodeFormat {
Alpha2,
Alpha3,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
#[builder(start_fn = init, on(String, into))]
pub struct Country {
pub iso: String,
pub iso3: String,
pub iso_numeric: String,
pub fips: String,
pub name: String,
pub capital: String,
pub area: Option<u64>,
pub population: Option<u64>,
pub continent: String,
pub top_level_domain: String,
pub currency_code: String,
pub currency_name: String,
pub country_code: String,
pub postal_code_format: String,
pub postal_code_regex: String,
pub languages: Vec<String>,
pub identifier: Option<u64>,
pub neighbours: Vec<String>,
pub equivalent_fips_code: Option<String>,
}
impl Countries {
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl Searchable<Country> for Countries {
fn contains(&self, value: &str) -> bool {
let trimmed = value.trim();
!trimmed.is_empty()
&& self.0.iter().any(|country| {
let Country { iso, iso3, .. } = country;
iso.eq_ignore_ascii_case(trimmed) || iso3.eq_ignore_ascii_case(trimmed)
})
}
fn find_by_iso(&self, value: impl Into<String>) -> Option<Country> {
let trimmed = value.into().trim().to_string();
self.0
.iter()
.find(|country| {
let Country { iso, iso3, .. } = country;
iso.eq_ignore_ascii_case(&trimmed) || iso3.eq_ignore_ascii_case(&trimmed)
})
.cloned()
}
fn find_by_name(&self, value: impl Into<String>) -> Option<Country> {
let trimmed = value.into().trim().to_string();
self.0
.iter()
.find(|country| {
let Country { name, .. } = country;
name.eq_ignore_ascii_case(&trimmed)
})
.cloned()
}
}
impl From<Vec<Country>> for Countries {
fn from(value: Vec<Country>) -> Self {
Self(value)
}
}
impl From<&str> for Countries {
fn from(value: &str) -> Self {
parse(value)
}
}
impl From<String> for Countries {
fn from(value: String) -> Self {
parse(&value)
}
}
impl GeonamesParser for Constant {
fn country_codes(format: CodeFormat) -> Vec<String> {
let data = Self::country_data();
country_codes(data, format)
}
fn country_data() -> Countries {
let text = Self::from_asset("geonames.tsv").unwrap_or_default();
parse(&text)
}
fn languages(format: CodeFormat) -> Vec<String> {
let data = Self::country_data();
languages(data, format)
}
}
fn country_codes(data: Countries, format: CodeFormat) -> Vec<String> {
let mut codes = data
.0
.into_iter()
.map(|country: Country| {
let code = match format {
| CodeFormat::Alpha2 => country.iso,
| CodeFormat::Alpha3 => country.iso3,
};
code.trim().to_lowercase()
})
.filter(|value| !value.is_empty())
.collect::<Vec<String>>();
codes.sort();
codes.dedup();
codes
}
fn languages(data: Countries, format: CodeFormat) -> Vec<String> {
let mut languages = data
.0
.into_iter()
.flat_map(|country: Country| {
country
.languages
.into_iter()
.map(|value| value.trim().to_lowercase())
.filter(|value| !value.is_empty())
.collect::<Vec<String>>()
})
.filter(|value| match format {
| CodeFormat::Alpha2 => value.len() == 2,
| CodeFormat::Alpha3 => value.contains('-'),
})
.collect::<Vec<String>>();
languages.sort();
languages.dedup();
languages
}
fn parse(data: &str) -> Countries {
fn parse_optional_string_field(value: &str) -> Option<String> {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
fn parse_u64_field(value: &str) -> Option<u64> {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
trimmed.parse::<u64>().ok()
}
}
fn list_field(value: &str) -> Vec<String> {
value
.split(',')
.map(|item| item.trim())
.filter(|item| !item.is_empty())
.map(|item| item.to_string())
.collect()
}
fn string_field(columns: &[&str], index: usize) -> String {
columns.get(index).map(|s| s.trim().to_string()).unwrap_or_default()
}
data.lines()
.filter(|line| {
let line = line.trim();
!line.is_empty() && !line.starts_with('#')
})
.filter(|line| !line.starts_with("ISO\t"))
.map(|line| {
let columns: Vec<&str> = line.split('\t').collect();
Country {
iso: string_field(&columns, 0),
iso3: string_field(&columns, 1),
iso_numeric: string_field(&columns, 2),
fips: string_field(&columns, 3),
name: string_field(&columns, 4),
capital: string_field(&columns, 5),
area: columns.get(6).and_then(|s| parse_u64_field(s)),
population: columns.get(7).and_then(|s| parse_u64_field(s)),
continent: string_field(&columns, 8),
top_level_domain: string_field(&columns, 9),
currency_code: string_field(&columns, 10),
currency_name: string_field(&columns, 11),
country_code: string_field(&columns, 12),
postal_code_format: string_field(&columns, 13),
postal_code_regex: string_field(&columns, 14),
languages: columns.get(15).map(|s| list_field(s)).unwrap_or_default(),
identifier: columns.get(16).and_then(|s| parse_u64_field(s)),
neighbours: columns.get(17).map(|s| list_field(s)).unwrap_or_default(),
equivalent_fips_code: columns.get(18).and_then(|s| parse_optional_string_field(s)),
}
})
.collect::<Vec<Country>>()
.into()
}