Skip to main content

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}