1use crate::error::CoreError;
36use serde_with::{DeserializeFromStr, SerializeDisplay};
37use std::{env::VarError, fmt::Display, str::FromStr};
38
39#[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
54pub 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
66impl 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 s.len() == 2 && s.chars().all(|c| c.is_ascii_uppercase())
139 }
140}
141
142impl 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
159const CC_CODE_BASIS: u32 = 'A' as u32;
164
165pub(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
174fn 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#[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()); assert!("USA".parse::<CountryCode>().is_err()); assert!("1X".parse::<CountryCode>().is_err()); assert!("".parse::<CountryCode>().is_err()); }
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 assert_eq!(
247 CountryCode::from_env_named("RFHAM_COUNTRY_ABSENT_TEST_ONLY").unwrap(),
248 None
249 );
250 }
251}