ipflag 0.1.0

Human-friendly IP -> country flag display core (resolver-pluggable, no data bundled).
Documentation
//! # IP Flag (ipflag)
//!
//! IP Flag is a small, resolver-pluggable Rust crate that displays a human-friendly
//! country indicator for an IP address (flag emoji + country code).
//!
//! This crate is intentionally **not** a geolocation database and does **not**
//! contact external services. It only provides:
//!
//! - IP parsing (`parse_ip`)
//! - IP scope classification (`classify_ip`)
//! - A resolver interface (`IpResolver`) for IP β†’ CountryCode lookup
//! - Display helpers (`IpTag`, `TagFormat`)
//!
//! ## Important concept: "Resolver"
//!
//! This crate does **not** know what country an IP belongs to by itself.
//! To get real country results, you must provide a resolver implementation.
//!
//! A resolver can be backed by anything:
//! - A local GeoIP database (e.g., MaxMind mmdb)
//! - A custom IP-to-country mapping
//! - A reverse-proxy header mapping (if you trust your infra)
//! - Any other internal source
//!
//! The core stays stable and maintenance-free; data sources can be built as separate crates.
//!
//! ## Quick start (no country resolution)
//!
//! Even without a resolver, IP Flag can still provide useful UI output for private/special IPs.
//!
//! ```rust
//! use ipflag::{tag_ip, NoopResolver};
//!
//! let t1 = tag_ip(&NoopResolver, "192.168.0.1").unwrap();
//! assert_eq!(t1.to_string(), "🏠 PRIVATE");
//!
//! let t2 = tag_ip(&NoopResolver, "8.8.8.8").unwrap();
//! assert_eq!(t2.to_string(), "🌐 UNKNOWN");
//! ```
//!
//! ## Quick start (with a resolver)
//!
//! You provide a resolver that returns a [`CountryCode`] for public IPs.
//! This example is intentionally hard-coded (demo only):
//!
//! ```rust
//! use ipflag::{CountryCode, IpResolver, tag_ip};
//! use std::net::IpAddr;
//!
//! struct DemoResolver;
//! impl IpResolver for DemoResolver {
//!     type Error = core::convert::Infallible;
//!     fn resolve(&self, _ip: IpAddr) -> Result<Option<CountryCode>, Self::Error> {
//!         Ok(CountryCode::new("KR"))
//!     }
//! }
//!
//! let t = tag_ip(&DemoResolver, "8.8.8.8").unwrap();
//! assert_eq!(t.to_string(), "πŸ‡°πŸ‡· KR");
//! ```
//!
//! In real usage, your resolver should consult real data (e.g., GeoIP database).
//!
//! ## Non-goals
//!
//! IP Flag does not:
//! - Detect manipulation / label behavior as suspicious
//! - Identify people or guarantee nationality
//! - Provide geolocation data by itself
//!
//! It only helps make IP-based patterns easier to scan visually.

pub mod country;
pub mod error;
pub mod ip;
pub mod resolver;
pub mod tag;

pub use country::CountryCode;
pub use error::IpflagError;
pub use ip::{classify_ip, parse_ip, IpScope};
pub use resolver::{IpResolver, NoopResolver};
pub use tag::{IpTag, TagFormat};

use std::net::IpAddr;

/// Tag an IP string into an [`IpTag`].
///
/// This function:
/// 1) Parses the IP string
/// 2) Classifies it into [`IpScope`]
/// 3) Only if it is `Public`, asks the resolver for a [`CountryCode`]
///
/// Private and special IP ranges do not call the resolver (by design).
///
/// # Examples
///
/// ```rust
/// use ipflag::{tag_ip, NoopResolver};
///
/// let tag = tag_ip(&NoopResolver, "192.168.0.1").unwrap();
/// assert_eq!(tag.to_string(), "🏠 PRIVATE");
/// ```
///
/// With a resolver:
///
/// ```rust
/// use ipflag::{tag_ip, CountryCode, IpResolver};
/// use std::net::IpAddr;
///
/// struct AlwaysUS;
/// impl IpResolver for AlwaysUS {
///     type Error = core::convert::Infallible;
///     fn resolve(&self, _ip: IpAddr) -> Result<Option<CountryCode>, Self::Error> {
///         Ok(CountryCode::new("US"))
///     }
/// }
///
/// let tag = tag_ip(&AlwaysUS, "8.8.8.8").unwrap();
/// assert_eq!(tag.to_string(), "πŸ‡ΊπŸ‡Έ US");
/// ```
pub fn tag_ip<R: IpResolver>(
    resolver: &R,
    ip: &str,
) -> Result<IpTag, IpflagError<R::Error>> {
    let addr = parse_ip(ip).map_err(IpflagError::InvalidIp)?;
    tag_addr(resolver, addr)
}

/// Tag an already-parsed [`IpAddr`] into an [`IpTag`].
///
/// See [`tag_ip`] for the high-level behavior.
///
/// # Notes
/// - `Private` / `Special` never calls the resolver.
/// - `Public` calls the resolver and returns either `Country(code)` or `Unknown`.
pub fn tag_addr<R: IpResolver>(
    resolver: &R,
    addr: IpAddr,
) -> Result<IpTag, IpflagError<R::Error>> {
    match classify_ip(addr) {
        IpScope::Private => Ok(IpTag::Private),
        IpScope::Special => Ok(IpTag::Special),
        IpScope::Public => {
            let code = resolver.resolve(addr).map_err(IpflagError::Resolver)?;
            Ok(match code {
                Some(cc) => IpTag::Country(cc),
                None => IpTag::Unknown,
            })
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    struct StaticKR;
    impl IpResolver for StaticKR {
        type Error = core::convert::Infallible;
        fn resolve(&self, _ip: IpAddr) -> Result<Option<CountryCode>, Self::Error> {
            Ok(CountryCode::new("KR"))
        }
    }

    #[test]
    fn flag_from_code() {
        let kr = CountryCode::new("kr").unwrap();
        assert_eq!(kr.flag().unwrap(), "πŸ‡°πŸ‡·");
    }

    #[test]
    fn tag_public_with_resolver() {
        let t = tag_ip(&StaticKR, "8.8.8.8").unwrap();
        assert_eq!(t.to_string(), "πŸ‡°πŸ‡· KR");
    }

    #[test]
    fn tag_private() {
        let t = tag_ip(&NoopResolver, "192.168.0.1").unwrap();
        assert_eq!(t.to_string(), "🏠 PRIVATE");
    }
}