ftracker_identifiers/country.rs
1//! ISO 3166-1 alpha-2 country codes: the two letter codes that identify countries, dependent
2//! territories, and special areas of geographical interest.
3//!
4//! This module provides the validated Rust representation ([`CountryCode`]) together with the
5//! parsing, validation, and error types that surround it. It accepts the canonical two letter form
6//! (optionally surrounded by whitespace, in any ASCII case), normalizes it, and guarantees that any
7//! constructed [`CountryCode`] is a code that ISO 3166-1 officially assigns. There is no partially
8//! validated state. If you hold a [`CountryCode`], it is valid.
9//!
10//! # What this type represents
11//!
12//! A country code is two uppercase ASCII letters, for example `US`, `BR`, or `GB`. The code
13//! identifies a country or territory. This crate stores only the code itself. It does not carry the
14//! country name, the alpha-3 code, or the numeric code, and it does not model subdivisions.
15//!
16//! [`CountryCode`] stores the two characters as normalized uppercase ASCII. It exposes borrowed
17//! accessors for the raw bytes ([`CountryCode::as_bytes`]) and for the whole value
18//! ([`CountryCode::as_str`]).
19//!
20//! # Validation rules
21//!
22//! A country code has no check digit. It is valid exactly when it is one of the codes ISO 3166-1
23//! officially assigns. This crate embeds that set as a compile time bitmap. Every fallible
24//! constructor runs the same rules, in order, and each maps to one [`CountryCodeError`] variant:
25//!
26//! 1. Length: after surrounding whitespace is trimmed, the input must contain exactly two
27//! characters ([`CountryCodeError::InvalidLength`]). [`CountryCode::parse`] rejects empty input
28//! first ([`CountryCodeError::Empty`]).
29//! 2. Character class: both positions must be an uppercase ASCII letter
30//! ([`CountryCodeError::InvalidCharacter`]).
31//! 3. Assignment: the two letters together must be an officially assigned code
32//! ([`CountryCodeError::Unassigned`]).
33//!
34//! Only the assigned codes are recognized. Reserved codes such as `EU` and `UK`, and the user
35//! assigned ranges, are not accepted. Codes that were once used and later withdrawn are not
36//! accepted either.
37//!
38//! # Design notes
39//!
40//! * No invalid state is representable. The only field of [`CountryCode`] is private. Every way to
41//! obtain one ([`CountryCode::parse`], [`CountryCode::new`], [`CountryCode::from_bytes`],
42//! [`FromStr`], and [`TryFrom<&str>`]) runs full validation. There is no unchecked constructor.
43//! * It is zero allocation and `Copy`. [`CountryCode`] is a two byte value that wraps `[u8; 2]`. It
44//! works in `no_std` environments. Parsing, validation, and every accessor operate on the stack.
45//! The assignment check computes one array index and tests one bit.
46//! * Ordering and hashing operate over the raw ASCII bytes. This matches [`str`] ordering on
47//! [`CountryCode::as_str`], which is lexicographic and carries no geographic meaning.
48//! * It is safe to use as a map or set key. [`CountryCode`] implements [`Eq`] and [`Hash`]
49//! consistently with [`PartialEq`], so it works as a `HashMap` or `HashSet` key, and as a
50//! `BTreeMap` or `BTreeSet` key, out of the box.
51//!
52//! # Feature flags
53//!
54//! The optional integrations are off by default and purely additive. Enabling one never changes the
55//! behavior of [`CountryCode::parse`] or the validation rules above:
56//!
57//! * `serde`: (de)serializes [`CountryCode`] as its two letter string, for example `"US"`.
58//! Deserialization re-runs full validation, so an untrusted payload can never produce an invalid
59//! [`CountryCode`].
60//! * `schemars`: implements `JsonSchema` for [`CountryCode`], describing it as a pattern
61//! constrained string (`^[A-Z]{2}$`). The pattern is structural. It cannot express which two
62//! letter codes are assigned, so validity is enforced on deserialization. Implies `serde`.
63//! * `arbitrary`: implements `Arbitrary` for [`CountryCode`], generating officially assigned codes
64//! for fuzz targets.
65//! * `proptest`: exposes reusable `proptest` strategies (`ftracker_identifiers::country::proptest`,
66//! when this feature is enabled) for generating valid [`CountryCode`] values.
67//!
68//! # Error handling
69//!
70//! Every fallible constructor returns [`CountryCodeError`], which is `Clone + PartialEq + Eq` and
71//! implements [`core::error::Error`] and [`core::fmt::Display`], so it composes with `?` and with
72//! error aggregation crates alike:
73//!
74//! ```
75//! use ftracker_identifiers::{CountryCode, CountryCodeError};
76//!
77//! match CountryCode::parse("ZZ") {
78//! Ok(code) => println!("valid: {code}"),
79//! Err(CountryCodeError::Unassigned { code }) => {
80//! println!("not assigned: {}{}", code[0], code[1]);
81//! }
82//! Err(other) => println!("rejected: {other}"),
83//! }
84//! ```
85//!
86//! # Examples
87//!
88//! ```
89//! use ftracker_identifiers::CountryCode;
90//!
91//! let code = CountryCode::parse("us").unwrap(); // lowercase is folded automatically
92//! assert_eq!(code.as_str(), "US");
93//! assert_eq!(code.as_bytes(), b"US");
94//! ```
95//!
96//! Sorting and deduplicating a batch of codes, for example after importing them from a spreadsheet:
97//!
98//! ```
99//! use ftracker_identifiers::CountryCode;
100//!
101//! let mut codes: Vec<CountryCode> = ["US", "BR", "US"]
102//! .into_iter()
103//! .map(|s| CountryCode::parse(s).unwrap())
104//! .collect();
105//! codes.sort();
106//! codes.dedup();
107//! assert_eq!(codes.len(), 2);
108//! ```
109
110mod error;
111mod fmt;
112mod parser;
113mod table;
114mod validation;
115
116#[cfg(feature = "serde")]
117mod serde;
118
119#[cfg(feature = "schemars")]
120mod schema;
121
122#[cfg(feature = "arbitrary")]
123mod arbitrary;
124
125#[cfg(any(test, feature = "proptest"))]
126pub mod proptest;
127
128#[cfg(test)]
129mod tests;
130
131pub use error::CountryCodeError;
132
133use core::convert::TryFrom;
134use core::str::{FromStr, from_utf8_unchecked};
135
136/// A validated ISO 3166-1 alpha-2 country code.
137///
138/// `CountryCode` is a two byte, `Copy`, allocation free value object. Once constructed, it is
139/// guaranteed to be a code that ISO 3166-1 officially assigns. There is no way to get a
140/// `CountryCode` that has not passed validation.
141///
142/// Internally, the code is stored as two raw uppercase ASCII letters (`'A'..='Z'`).
143///
144/// # Constructing a `CountryCode`
145///
146/// * [`CountryCode::parse`] and [`CountryCode::new`] accept two character strings, in any ASCII
147/// case, trimmed of surrounding whitespace.
148/// * [`CountryCode::from_bytes`] accepts exactly two pre normalized uppercase ASCII bytes.
149/// * [`FromStr`] and [`TryFrom<&str>`] behave like `parse`, for use in generic code.
150///
151/// All of them run the same validation and return [`CountryCodeError`] on failure. See the
152/// [module level documentation](self) for the validation rules and design rationale.
153#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
154#[must_use = "a parsed CountryCode should be used; discarding it wastes the validation work"]
155pub struct CountryCode {
156 bytes: [u8; 2],
157}
158
159impl CountryCode {
160 /// Parses a country code from a string.
161 ///
162 /// The parser trims surrounding whitespace and folds ASCII letters to uppercase before
163 /// validation. This is the primary constructor. [`CountryCode::new`], [`FromStr`], and
164 /// [`TryFrom<&str>`] all delegate to it.
165 ///
166 /// # Errors
167 ///
168 /// Returns [`CountryCodeError`] if the input is empty, does not contain exactly two characters
169 /// after trimming, contains a non letter character, or names a code that ISO 3166-1 does not
170 /// officially assign.
171 ///
172 /// # Examples
173 ///
174 /// ```
175 /// use ftracker_identifiers::CountryCode;
176 ///
177 /// assert!(CountryCode::parse("US").is_ok());
178 /// assert!(CountryCode::parse("us").is_ok()); // lowercase is folded automatically
179 /// assert!(CountryCode::parse(" BR ").is_ok()); // surrounding whitespace is trimmed
180 /// assert!(CountryCode::parse("ZZ").is_err()); // well formed but not assigned
181 /// ```
182 pub fn parse(input: &str) -> Result<Self, CountryCodeError> {
183 let candidate = parser::normalize(input)?;
184 Self::from_bytes(candidate)
185 }
186
187 /// Alias for [`CountryCode::parse`].
188 ///
189 /// # Errors
190 ///
191 /// See [`CountryCode::parse`].
192 ///
193 /// # Examples
194 ///
195 /// ```
196 /// use ftracker_identifiers::CountryCode;
197 ///
198 /// assert_eq!(CountryCode::new("US"), CountryCode::parse("US"));
199 /// ```
200 #[inline]
201 pub fn new(input: &str) -> Result<Self, CountryCodeError> {
202 Self::parse(input)
203 }
204
205 /// Constructs a `CountryCode` directly from two raw ASCII bytes.
206 ///
207 /// Each byte must already be an uppercase letter. Use [`CountryCode::parse`] if the input might
208 /// contain surrounding whitespace or lowercase letters.
209 ///
210 /// # Errors
211 ///
212 /// Returns [`CountryCodeError`] under the same conditions as [`CountryCode::parse`], except that
213 /// length is guaranteed by the `[u8; 2]` type itself: [`CountryCodeError::InvalidLength`] cannot
214 /// occur here.
215 ///
216 /// # Examples
217 ///
218 /// ```
219 /// use ftracker_identifiers::CountryCode;
220 ///
221 /// let code = CountryCode::from_bytes(*b"US").unwrap();
222 /// assert_eq!(code.as_str(), "US");
223 ///
224 /// // A well formed but unassigned code is rejected just like it would be through `parse`.
225 /// assert!(CountryCode::from_bytes(*b"ZZ").is_err());
226 /// ```
227 pub fn from_bytes(bytes: [u8; 2]) -> Result<Self, CountryCodeError> {
228 validation::validate(&bytes)?;
229 Ok(CountryCode { bytes })
230 }
231
232 /// Returns the two raw ASCII bytes backing this code (for example, `b"US"`).
233 ///
234 /// # Examples
235 ///
236 /// ```
237 /// use ftracker_identifiers::CountryCode;
238 ///
239 /// let code = CountryCode::parse("US").unwrap();
240 /// assert_eq!(code.as_bytes(), b"US");
241 /// ```
242 #[inline]
243 #[must_use]
244 pub fn as_bytes(&self) -> &[u8; 2] {
245 &self.bytes
246 }
247
248 /// Returns the two character country code as a `&str`.
249 ///
250 /// This never allocates. The bytes are guaranteed to be valid ASCII by construction.
251 ///
252 /// # Examples
253 ///
254 /// ```
255 /// use ftracker_identifiers::CountryCode;
256 ///
257 /// let code = CountryCode::parse("US").unwrap();
258 /// assert_eq!(code.as_str(), "US");
259 /// ```
260 #[inline]
261 #[must_use]
262 pub fn as_str(&self) -> &str {
263 // SAFETY: `CountryCode::from_bytes` guarantees both bytes are uppercase ASCII letters.
264 unsafe { from_utf8_unchecked(&self.bytes) }
265 }
266}
267
268impl FromStr for CountryCode {
269 type Err = CountryCodeError;
270
271 /// Delegates to [`CountryCode::parse`], enabling `input.parse::<CountryCode>()` and use in
272 /// generic code bounded by [`FromStr`].
273 fn from_str(s: &str) -> Result<Self, Self::Err> {
274 Self::parse(s)
275 }
276}
277
278impl TryFrom<&str> for CountryCode {
279 type Error = CountryCodeError;
280
281 /// Delegates to [`CountryCode::parse`], enabling `CountryCode::try_from(input)` and use in
282 /// generic code bounded by [`TryFrom<&str>`].
283 fn try_from(value: &str) -> Result<Self, Self::Error> {
284 Self::parse(value)
285 }
286}
287
288impl TryFrom<[u8; 2]> for CountryCode {
289 type Error = CountryCodeError;
290
291 /// Delegates to [`CountryCode::from_bytes`]. The two bytes must already be pre normalized
292 /// uppercase ASCII letters.
293 fn try_from(value: [u8; 2]) -> Result<Self, Self::Error> {
294 Self::from_bytes(value)
295 }
296}
297
298impl TryFrom<&[u8]> for CountryCode {
299 type Error = CountryCodeError;
300
301 /// Validates a byte slice as a country code. The slice must be exactly two pre normalized
302 /// uppercase ASCII bytes; any other length yields [`CountryCodeError::InvalidLength`]. Once the
303 /// length is confirmed, this behaves like [`CountryCode::from_bytes`].
304 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
305 let bytes: [u8; 2] = value
306 .try_into()
307 .map_err(|_| CountryCodeError::InvalidLength { found: value.len() })?;
308 Self::from_bytes(bytes)
309 }
310}
311
312impl PartialEq<str> for CountryCode {
313 /// Compares against a string slice by its canonical two letter representation.
314 fn eq(&self, other: &str) -> bool {
315 self.as_str() == other
316 }
317}
318
319impl PartialEq<&str> for CountryCode {
320 /// Compares against a string slice by its canonical two letter representation.
321 fn eq(&self, other: &&str) -> bool {
322 self.as_str() == *other
323 }
324}
325
326impl PartialEq<CountryCode> for str {
327 fn eq(&self, other: &CountryCode) -> bool {
328 self == other.as_str()
329 }
330}
331
332impl PartialEq<CountryCode> for &str {
333 fn eq(&self, other: &CountryCode) -> bool {
334 *self == other.as_str()
335 }
336}
337
338impl AsRef<[u8]> for CountryCode {
339 /// Equivalent to [`CountryCode::as_bytes`], borrowed as a slice.
340 fn as_ref(&self) -> &[u8] {
341 &self.bytes
342 }
343}
344
345impl AsRef<str> for CountryCode {
346 /// Equivalent to [`CountryCode::as_str`].
347 fn as_ref(&self) -> &str {
348 self.as_str()
349 }
350}