ipflag 0.1.0

Human-friendly IP -> country flag display core (resolver-pluggable, no data bundled).
Documentation
use core::fmt;

/// A 2-letter ISO 3166-1 alpha-2 country code (e.g., "KR", "US", "CN").
///
/// This is intentionally small and stable:
/// - IP Flag does not ship a country database.
/// - IP Flag does not ship country names.
/// - Any resolver can supply a country code.
///
/// The main job of this type is:
/// - validation (must be exactly 2 ASCII letters)
/// - formatting / display
/// - generating a flag emoji via Unicode regional indicators
///
/// # Example
///
/// ```rust
/// use ipflag::CountryCode;
///
/// let kr = CountryCode::new("kr").unwrap();
/// assert_eq!(kr.to_string(), "KR");
/// assert_eq!(kr.flag().unwrap(), "πŸ‡°πŸ‡·");
/// ```
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct CountryCode {
    /// First ASCII letter (A–Z)
    pub a: u8,
    /// Second ASCII letter (A–Z)
    pub b: u8,
}

impl CountryCode {
    /// Create a [`CountryCode`] from a 2-letter string (case-insensitive).
    ///
    /// Returns `None` if:
    /// - the length is not exactly 2
    /// - either char is not ASCII A-Z
    ///
    /// # Example
    ///
    /// ```rust
    /// use ipflag::CountryCode;
    ///
    /// assert!(CountryCode::new("KR").is_some());
    /// assert!(CountryCode::new("kr").is_some());
    /// assert!(CountryCode::new("KOR").is_none());
    /// assert!(CountryCode::new("1R").is_none());
    /// ```
    pub fn new(code: &str) -> Option<Self> {
        let bytes = code.as_bytes();
        if bytes.len() != 2 {
            return None;
        }

        let a = bytes[0].to_ascii_uppercase();
        let b = bytes[1].to_ascii_uppercase();

        if !(b'A'..=b'Z').contains(&a) || !(b'A'..=b'Z').contains(&b) {
            return None;
        }
        Some(Self { a, b })
    }

    /// Returns the raw uppercase ASCII bytes (e.g., `[b'K', b'R']`).
    ///
    /// This is useful if you want to avoid allocations.
    pub fn as_bytes(&self) -> [u8; 2] {
        [self.a, self.b]
    }

    /// Returns the uppercase country code as an owned `String` (e.g., `"KR"`).
    ///
    /// Note: We intentionally do not provide `&'static str` here because
    /// the code is stored as bytes, not as a static string.
    pub fn to_string(&self) -> String {
        let mut s = String::with_capacity(2);
        s.push(self.a as char);
        s.push(self.b as char);
        s
    }

    /// Convert the country code into a flag emoji string (e.g., `"πŸ‡°πŸ‡·"`).
    ///
    /// This uses Unicode "Regional Indicator Symbols".
    /// If the code is valid (A-Z), this should always return `Some`.
    ///
    /// # Example
    ///
    /// ```rust
    /// use ipflag::CountryCode;
    ///
    /// let us = CountryCode::new("US").unwrap();
    /// assert_eq!(us.flag().unwrap(), "πŸ‡ΊπŸ‡Έ");
    /// ```
    pub fn flag(&self) -> Option<String> {
        let base = 0x1F1E6u32; // Regional Indicator Symbol Letter A
        let a = base + (self.a as u32 - 'A' as u32);
        let b = base + (self.b as u32 - 'A' as u32);
        let ca = char::from_u32(a)?;
        let cb = char::from_u32(b)?;
        let mut out = String::new();
        out.push(ca);
        out.push(cb);
        Some(out)
    }
}

impl fmt::Display for CountryCode {
    /// Displays the uppercase 2-letter code, e.g. `"KR"`.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}{}", self.a as char, self.b as char)
    }
}