Skip to main content

rfham_core/
countries.rs

1//! ISO 3166-1 alpha-2 country codes.
2//!
3//! [`CountryCode`] is a two-uppercase-letter code validated at construction time.
4//! It can be read from an environment variable ([`ENVVAR_COUNTRY_CODE`]) for
5//! locale-aware behaviour in library consumers.
6//!
7//! Internally the two letters are packed into a [`u16`] via [`CountryCode::to_numeric`]
8//! and unpacked with [`CountryCode::try_from`], which allows compact storage when needed.
9//!
10//! # Examples
11//!
12//! ```rust
13//! use rfham_core::country::CountryCode;
14//! use std::str::FromStr;
15//!
16//! let us = CountryCode::from_str("US").unwrap();
17//! assert_eq!(us.to_string(), "US");
18//!
19//! // Numeric round-trip
20//! let n: u16 = us.to_numeric();
21//! let back = CountryCode::try_from(n).unwrap();
22//! assert_eq!(us, back);
23//! ```
24//!
25//! Invalid codes are rejected:
26//!
27//! ```rust
28//! use rfham_core::country::CountryCode;
29//!
30//! assert!("us".parse::<CountryCode>().is_err());   // must be uppercase
31//! assert!("USA".parse::<CountryCode>().is_err());  // must be exactly 2 chars
32//! assert!("1X".parse::<CountryCode>().is_err());   // must be letters
33//! ```
34
35use crate::error::CoreError;
36use serde_with::{DeserializeFromStr, SerializeDisplay};
37use std::{env::VarError, fmt::Display, str::FromStr};
38
39// ------------------------------------------------------------------------------------------------
40// Public Macros
41// ------------------------------------------------------------------------------------------------
42
43// ------------------------------------------------------------------------------------------------
44// Public Types
45// ------------------------------------------------------------------------------------------------
46
47#[derive(Clone, Debug, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
48pub struct CountryCode(String);
49
50pub const ENVVAR_COUNTRY_CODE: &str = "RFHAM_COUNTRY";
51
52pub type CountryCodeNumeric = u16;
53
54// ------------------------------------------------------------------------------------------------
55// Public Functions
56// ------------------------------------------------------------------------------------------------
57
58pub fn country_code_us() -> CountryCode {
59    CountryCode::new_unchecked("US")
60}
61
62pub fn country_code_uk() -> CountryCode {
63    CountryCode::new_unchecked("UK")
64}
65
66// ------------------------------------------------------------------------------------------------
67// Private Macros
68// ------------------------------------------------------------------------------------------------
69
70// ------------------------------------------------------------------------------------------------
71// Private Types
72// ------------------------------------------------------------------------------------------------
73
74// ------------------------------------------------------------------------------------------------
75// Implementations
76// ------------------------------------------------------------------------------------------------
77
78impl Display for CountryCode {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        write!(f, "{}", self.0)
81    }
82}
83
84impl FromStr for CountryCode {
85    type Err = CoreError;
86
87    fn from_str(s: &str) -> Result<Self, Self::Err> {
88        if Self::is_valid(s) {
89            Ok(Self(s.to_string()))
90        } else {
91            Err(CoreError::InvalidValueFromStr(s.to_string(), "CountryCode"))
92        }
93    }
94}
95
96impl From<CountryCode> for String {
97    fn from(value: CountryCode) -> Self {
98        value.0
99    }
100}
101
102impl AsRef<str> for CountryCode {
103    fn as_ref(&self) -> &str {
104        self.0.as_ref()
105    }
106}
107
108impl CountryCode {
109    pub(crate) fn new_unchecked(s: &str) -> Self {
110        Self(s.to_string())
111    }
112
113    pub fn from_env() -> Result<Option<Self>, CoreError> {
114        Self::from_env_named(ENVVAR_COUNTRY_CODE)
115    }
116
117    pub fn from_env_named(envvar_name: &str) -> Result<Option<Self>, CoreError> {
118        match std::env::var(envvar_name) {
119            Ok(value) => Ok(Some(Self::from_str(&value)?)),
120            Err(VarError::NotPresent) => Ok(None),
121            Err(VarError::NotUnicode(value)) => Err(CoreError::InvalidValueFromStr(
122                format!("{:#?}", value),
123                "CountryCode",
124            )),
125        }
126    }
127
128    pub fn as_str(&self) -> &str {
129        self.0.as_str()
130    }
131
132    pub fn to_numeric(&self) -> CountryCodeNumeric {
133        country_code_coded(self.0.as_str())
134    }
135
136    pub fn is_valid(s: &str) -> bool {
137        // TODO: validate correct combinations, this is good enough for now.
138        s.len() == 2 && s.chars().all(|c| c.is_ascii_uppercase())
139    }
140}
141
142// ------------------------------------------------------------------------------------------------
143
144impl From<CountryCode> for CountryCodeNumeric {
145    fn from(country_code: CountryCode) -> Self {
146        country_code_coded(country_code.as_str())
147    }
148}
149
150impl TryFrom<CountryCodeNumeric> for CountryCode {
151    type Error = CoreError;
152
153    fn try_from(value: CountryCodeNumeric) -> Result<Self, Self::Error> {
154        let country_code = country_code_decoded(value)?;
155        CountryCode::from_str(&country_code)
156    }
157}
158
159// ------------------------------------------------------------------------------------------------
160// Private Functions
161// ------------------------------------------------------------------------------------------------
162
163const CC_CODE_BASIS: u32 = 'A' as u32;
164
165// Internal only as this does not do any validation whatsoever.
166pub(crate) fn country_code_coded(s: &str) -> CountryCodeNumeric {
167    let pair: Vec<u16> = s
168        .chars()
169        .map(|c| (c as u32 - CC_CODE_BASIS) as u16)
170        .collect();
171    (pair[0] << 8) + pair[1]
172}
173
174// Internal only as this does not do any validation whatsoever.
175fn country_code_decoded(country_code: CountryCodeNumeric) -> Result<String, CoreError> {
176    Ok(vec![
177        char::from_u32((country_code >> 8) as u32 + CC_CODE_BASIS).ok_or(
178            CoreError::InvalidValue(country_code.to_string(), "CountryCode"),
179        )?,
180        char::from_u32((country_code & 0b11111111) as u32 + CC_CODE_BASIS).ok_or(
181            CoreError::InvalidValue(country_code.to_string(), "CountryCode"),
182        )?,
183    ]
184    .into_iter()
185    .collect::<String>())
186}
187
188// ------------------------------------------------------------------------------------------------
189// Sub-Modules
190// ------------------------------------------------------------------------------------------------
191
192// ------------------------------------------------------------------------------------------------
193// Unit Tests
194// ------------------------------------------------------------------------------------------------
195
196#[cfg(test)]
197mod test {
198    use super::{CountryCode, CountryCodeNumeric, country_code_coded, country_code_decoded};
199    use pretty_assertions::assert_eq;
200    use std::str::FromStr;
201
202    const VALID_MAPPINGS: &[(&str, CountryCodeNumeric)] =
203        &[("US", 5138_u16), ("GB", 1537), ("CN", 525)];
204
205    #[test]
206    fn country_code_to_number() {
207        for (string, numeric) in VALID_MAPPINGS {
208            assert_eq!(*numeric, country_code_coded(string));
209        }
210    }
211
212    #[test]
213    fn country_code_to_string() {
214        for (string, numeric) in VALID_MAPPINGS {
215            assert_eq!(string, &country_code_decoded(*numeric).unwrap().as_str());
216        }
217    }
218
219    #[test]
220    fn country_code_from_str_valid() {
221        assert!(CountryCode::from_str("US").is_ok());
222        assert!(CountryCode::from_str("JP").is_ok());
223        assert!(CountryCode::from_str("DE").is_ok());
224    }
225
226    #[test]
227    fn country_code_from_str_invalid() {
228        assert!("us".parse::<CountryCode>().is_err());   // lowercase
229        assert!("USA".parse::<CountryCode>().is_err());  // 3 chars
230        assert!("1X".parse::<CountryCode>().is_err());   // leading digit
231        assert!("".parse::<CountryCode>().is_err());     // empty
232    }
233
234    #[test]
235    fn country_code_numeric_roundtrip() {
236        for code in ["US", "GB", "JP", "CN", "DE"] {
237            let cc = CountryCode::from_str(code).unwrap();
238            let n = cc.to_numeric();
239            assert_eq!(cc, CountryCode::try_from(n).unwrap(), "roundtrip failed for {code}");
240        }
241    }
242
243    #[test]
244    fn country_code_from_env_absent() {
245        // Use a name that is guaranteed to be unset rather than mutating the environment.
246        assert_eq!(
247            CountryCode::from_env_named("RFHAM_COUNTRY_ABSENT_TEST_ONLY").unwrap(),
248            None
249        );
250    }
251}