Skip to main content

rfham_core/
country.rs

1//!
2//! Provides ..., a one-line description
3//!
4//! More detailed description
5//!
6//! # Examples
7//!
8//! ```rust
9//! ```
10//!
11
12use crate::error::CoreError;
13use serde_with::{DeserializeFromStr, SerializeDisplay};
14use std::{env::VarError, fmt::Display, str::FromStr};
15
16// ------------------------------------------------------------------------------------------------
17// Public Macros
18// ------------------------------------------------------------------------------------------------
19
20// ------------------------------------------------------------------------------------------------
21// Public Types
22// ------------------------------------------------------------------------------------------------
23
24#[derive(Clone, Debug, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
25pub struct CountryCode(String);
26
27pub const ENVVAR_COUNTRY_CODE: &str = "RFHAM_COUNTRY";
28
29pub type CountryCodeNumeric = u16;
30
31// ------------------------------------------------------------------------------------------------
32// Public Functions
33// ------------------------------------------------------------------------------------------------
34
35pub fn country_code_us() -> CountryCode {
36    CountryCode::new_unchecked("US")
37}
38
39pub fn country_code_uk() -> CountryCode {
40    CountryCode::new_unchecked("UK")
41}
42
43// ------------------------------------------------------------------------------------------------
44// Private Macros
45// ------------------------------------------------------------------------------------------------
46
47// ------------------------------------------------------------------------------------------------
48// Private Types
49// ------------------------------------------------------------------------------------------------
50
51// ------------------------------------------------------------------------------------------------
52// Implementations
53// ------------------------------------------------------------------------------------------------
54
55impl Display for CountryCode {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        write!(f, "{}", self.0)
58    }
59}
60
61impl FromStr for CountryCode {
62    type Err = CoreError;
63
64    fn from_str(s: &str) -> Result<Self, Self::Err> {
65        if Self::is_valid(s) {
66            Ok(Self(s.to_string()))
67        } else {
68            Err(CoreError::InvalidValueFromStr(s.to_string(), "CountryCode"))
69        }
70    }
71}
72
73impl From<CountryCode> for String {
74    fn from(value: CountryCode) -> Self {
75        value.0
76    }
77}
78
79impl AsRef<str> for CountryCode {
80    fn as_ref(&self) -> &str {
81        self.0.as_ref()
82    }
83}
84
85impl CountryCode {
86    pub(crate) fn new_unchecked(s: &str) -> Self {
87        Self(s.to_string())
88    }
89
90    pub fn from_env() -> Result<Option<Self>, CoreError> {
91        Self::from_env_named(ENVVAR_COUNTRY_CODE)
92    }
93
94    pub fn from_env_named(envvar_name: &str) -> Result<Option<Self>, CoreError> {
95        match std::env::var(envvar_name) {
96            Ok(value) => Ok(Some(Self::from_str(&value)?)),
97            Err(VarError::NotPresent) => Ok(None),
98            Err(VarError::NotUnicode(value)) => Err(CoreError::InvalidValueFromStr(
99                format!("{:#?}", value),
100                "CountryCode",
101            )),
102        }
103    }
104
105    pub fn as_str(&self) -> &str {
106        self.0.as_str()
107    }
108
109    pub fn to_numeric(&self) -> CountryCodeNumeric {
110        country_code_coded(self.0.as_str())
111    }
112
113    pub fn is_valid(s: &str) -> bool {
114        // TODO: validate correct combinations, this is good enough for now.
115        s.len() == 2 && s.chars().all(|c| c.is_ascii_uppercase())
116    }
117}
118
119// ------------------------------------------------------------------------------------------------
120
121impl From<CountryCode> for CountryCodeNumeric {
122    fn from(country_code: CountryCode) -> Self {
123        country_code_coded(country_code.as_str())
124    }
125}
126
127impl TryFrom<CountryCodeNumeric> for CountryCode {
128    type Error = CoreError;
129
130    fn try_from(value: CountryCodeNumeric) -> Result<Self, Self::Error> {
131        let country_code = country_code_decoded(value)?;
132        CountryCode::from_str(&country_code)
133    }
134}
135
136// ------------------------------------------------------------------------------------------------
137// Private Functions
138// ------------------------------------------------------------------------------------------------
139
140const CC_CODE_BASIS: u32 = 'A' as u32;
141
142// Internal only as this does not do any validation whatsoever.
143pub(crate) fn country_code_coded(s: &str) -> CountryCodeNumeric {
144    let pair: Vec<u16> = s
145        .chars()
146        .map(|c| (c as u32 - CC_CODE_BASIS) as u16)
147        .collect();
148    (pair[0] << 8) + pair[1]
149}
150
151// Internal only as this does not do any validation whatsoever.
152fn country_code_decoded(country_code: CountryCodeNumeric) -> Result<String, CoreError> {
153    Ok(vec![
154        char::from_u32((country_code >> 8) as u32 + CC_CODE_BASIS).ok_or(
155            CoreError::InvalidValue(country_code.to_string(), "CountryCode"),
156        )?,
157        char::from_u32((country_code & 0b11111111) as u32 + CC_CODE_BASIS).ok_or(
158            CoreError::InvalidValue(country_code.to_string(), "CountryCode"),
159        )?,
160    ]
161    .into_iter()
162    .collect::<String>())
163}
164
165// ------------------------------------------------------------------------------------------------
166// Sub-Modules
167// ------------------------------------------------------------------------------------------------
168
169// ------------------------------------------------------------------------------------------------
170// Unit Tests
171// ------------------------------------------------------------------------------------------------
172
173#[cfg(test)]
174mod test {
175    use super::{CountryCodeNumeric, country_code_coded, country_code_decoded};
176    use pretty_assertions::assert_eq;
177
178    const VALID_MAPPINGS: &[(&str, CountryCodeNumeric)] =
179        &[("US", 5138_u16), ("GB", 1537), ("CN", 525)];
180
181    #[test]
182    fn country_code_to_number() {
183        for (string, numeric) in VALID_MAPPINGS {
184            assert_eq!(*numeric, country_code_coded(string));
185        }
186    }
187
188    #[test]
189    fn country_code_to_string() {
190        for (string, numeric) in VALID_MAPPINGS {
191            assert_eq!(string, &country_code_decoded(*numeric).unwrap().as_str());
192        }
193    }
194}