monovm-whois-rust 1.0.0

Domain WHOIS and RDAP lookups with availability detection, structured record parsing, referral chasing, caching and rate limiting.
Documentation
//! Domain WHOIS and RDAP lookups, with availability detection you can audit.
//!
//! ```no_run
//! # #[cfg(feature = "blocking")] {
//! use monovm_whois::WhoisClient;
//!
//! let client = WhoisClient::new()?;
//! let lookup = client.lookup("example.com")?;
//!
//! println!("{} is {}", lookup.domain, lookup.availability());
//! if let Some(record) = &lookup.record {
//!     println!("registrar: {:?}", record.registrar);
//!     println!("expires:   {:?}", record.expires);
//! }
//! # }
//! # Ok::<(), monovm_whois::Error>(())
//! ```
//!
//! # The problem this crate is about
//!
//! WHOIS has no status codes. A registry answering "that domain is free", one
//! answering "you are querying too fast", and one answering "I do not serve that
//! suffix" all send prose over the same socket, and every one of them can contain
//! the word *available*. Libraries in this space overwhelmingly resolve that
//! ambiguity the same way — anything that is not recognisably a record is treated as
//! availability — which means a rate-limited registry reports its entire zone as
//! free to register.
//!
//! This crate never does that. A response that cannot be interpreted produces
//! [`Error::Inconclusive`], a refusal produces [`Error::Refused`], and neither is
//! ever an [`Availability`]. There is deliberately no `Availability::Unknown`,
//! because an uncertain answer that renders as "available" is the one outcome a
//! caller must not be handed.
//!
//! # How it is put together
//!
//! Six layers, each with one job and no knowledge of the others:
//!
//! | Layer | Responsibility | Key abstraction |
//! |---|---|---|
//! | [`domain`] | Validated values — a name, a suffix, a verdict | [`DomainName`], [`Tld`] |
//! | [`registry`] | Which registry serves a suffix, and how to reach it | [`RegistryProvider`](registry::RegistryProvider) |
//! | [`transport`] | Talking to servers. The only I/O in the crate | [`Transport`](transport::Transport) |
//! | [`cache`] | Not asking twice | [`ResponseCache`](cache::ResponseCache) |
//! | [`detect`] | Deciding what a response said | [`AvailabilityRule`](detect::AvailabilityRule) |
//! | [`parser`] | Turning a record into data | [`RecordParser`](parser::RecordParser) |
//!
//! [`client`] composes them. Every layer is a trait with a bundled implementation,
//! so a caller can replace any one of them — a private registry list, a transport
//! over a proxy, a Redis cache, an extra detection rule for a registry that words
//! things unusually — without forking the crate.
//!
//! # What you get
//!
//! - **Coverage.** 872 curated suffixes plus IANA's RDAP bootstrap registry, for
//!   over 1600 in total.
//! - **RDAP.** A full RFC 9083 client and typed model, used as a fallback when port
//!   43 refuses and preferred when [`Preference::Rdap`] is set. RDAP's 404 makes availability a fact rather than an inference.
//! - **Structured records.** [`WhoisRecord`] with typed dates,
//!   statuses, name servers and contacts, instead of the server's raw text.
//! - **Referral chasing.** Thin registries answer with a pointer to the registrar;
//!   following it is the difference between knowing a domain is taken and knowing
//!   who holds it.
//! - **Auditable verdicts.** Every answer names the rule that produced it and why,
//!   and [`WhoisClient::explain`] shows what every rule thought.
//! - **Rate limiting, retries and caching**, composed as transport decorators.
//! - **Both runtimes.** [`WhoisClient`] and [`AsyncWhoisClient`].
//!
//! # Features
//!
//! | Feature | Default | Gives you |
//! |---|---|---|
//! | `blocking` | yes | [`WhoisClient`] and the synchronous transports |
//! | `rdap` | yes | RDAP over HTTPS, and the typed [`rdap`] model |
//! | `parser` | yes | [`WhoisRecord`] and record parsing |
//! | `async` | no | [`AsyncWhoisClient`] and the Tokio transports |
//! | `iana-bootstrap` | no | Refreshing the RDAP registry from IANA at runtime |
//! | `cli` | no | The `monovm-whois` command line tool |
//! | `mock` | no | [`MockTransport`](transport::MockTransport), for your own tests |
//!
//! # A note on what a verdict means
//!
//! Availability detection over WHOIS is inference, and this crate is explicit about
//! how much. Every [`Verdict`](detect::Verdict) carries a
//! [`Confidence`](detect::Confidence): `Definitive` for a structured RDAP answer,
//! `High` for wording curated for that specific registry, `Medium` for a pattern
//! that generalises, `Low` for the one inference drawn from absence of evidence. A
//! caller who needs certainty can require `Definitive` and use
//! [`Preference::RdapOnly`].

#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
#![warn(clippy::all)]
#![forbid(unsafe_code)]

pub mod cache;
pub mod client;
pub mod detect;
pub mod domain;
pub mod error;
pub mod registry;
pub mod transport;

#[cfg(feature = "parser")]
pub mod parser;

#[cfg(feature = "rdap")]
pub mod rdap;

pub use domain::{Availability, DomainName, SuffixSplit, Tld};
pub use error::{DomainError, Error, Refusal, Result};

pub use client::{Explanation, Lookup, Preference, ReferralPolicy};

#[cfg(any(feature = "blocking", feature = "async"))]
pub use client::CheckReport;

#[cfg(feature = "blocking")]
pub use client::{Checker, WhoisClient, WhoisClientBuilder};

#[cfg(feature = "async")]
pub use client::{AsyncChecker, AsyncWhoisClient, AsyncWhoisClientBuilder};

#[cfg(feature = "parser")]
pub use parser::{Contact, WhoisRecord};

/// The crate version, from `Cargo.toml`.
pub const VERSION: &str = env!("CARGO_PKG_VERSION");

/// Look one domain up with a default client.
///
/// A convenience for a one-off query. Anything repeated should build a
/// [`WhoisClient`] and keep it: a client carries the rate limiter and the cache, and
/// a fresh one per query has neither.
///
/// ```no_run
/// # #[cfg(feature = "blocking")] {
/// let lookup = monovm_whois::lookup("example.com")?;
/// println!("{}", lookup.availability());
/// # }
/// # Ok::<(), monovm_whois::Error>(())
/// ```
#[cfg(feature = "blocking")]
pub fn lookup(domain: &str) -> Result<Lookup> {
    WhoisClient::new()?.lookup(domain)
}

/// Whether one domain is free to register, with a default client.
///
/// A premium or reserved name answers `false`; a query that could not be answered is
/// an error rather than `false`.
#[cfg(feature = "blocking")]
pub fn is_available(domain: &str) -> Result<bool> {
    WhoisClient::new()?.is_available(domain)
}

/// The availability of one domain, with a default client.
#[cfg(feature = "blocking")]
pub fn availability(domain: &str) -> Result<Availability> {
    WhoisClient::new()?.availability(domain)
}

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

    #[test]
    fn the_version_is_populated() {
        assert!(!VERSION.is_empty());
        assert!(VERSION.contains('.'));
    }

    #[test]
    fn the_bundled_data_covers_what_the_docs_claim() {
        let registry = registry::default_provider();
        let total = registry::RegistryProvider::tlds(&registry).len();

        assert!(
            total > 1600,
            "the crate documents over 1600 suffixes; found {total}"
        );
    }

    #[test]
    fn the_prelude_reexports_resolve() {
        // A compile-level check that the public surface named in the crate docs
        // actually exists at these paths.
        let _: fn(&str) -> Result<DomainName> = |s| DomainName::parse(s);
        let _: fn(&str) -> Result<Tld> = |s| Tld::parse(s);
        assert!(Availability::Available.is_available());
        assert_eq!(Preference::default(), Preference::Whois);
        assert!(ReferralPolicy::default().is_enabled());
    }
}