monovm-whois-rust 1.0.0

Domain WHOIS and RDAP lookups with availability detection, structured record parsing, referral chasing, caching and rate limiting.
Documentation
//! [`CachingTransport`]: serve a repeated question from a [`ResponseCache`].

use crate::cache::{CacheKey, ResponseCache};
use crate::error::Result;
use crate::registry::Endpoint;
use crate::transport::{Query, RawResponse, Transport};

#[cfg(feature = "async")]
use crate::transport::{AsyncTransport, BoxFuture};

/// Reads through a cache before touching the network.
///
/// Only successes are cached. A timeout or a rate-limit refusal is a fact about
/// one moment, not about the domain, and remembering it would turn a blip into a
/// five-minute outage.
///
/// ```
/// # #[cfg(feature = "blocking")] {
/// use std::time::Duration;
/// use monovm_whois::cache::MemoryCache;
/// use monovm_whois::transport::{CachingTransport, Whois43Transport};
///
/// let transport = CachingTransport::new(
///     Whois43Transport::default(),
///     MemoryCache::with_ttl(Duration::from_secs(600)),
/// );
/// # }
/// ```
#[derive(Debug)]
pub struct CachingTransport<T, C> {
    inner: T,
    cache: C,
}

impl<T, C> CachingTransport<T, C> {
    /// Wrap a transport with a cache.
    pub fn new(inner: T, cache: C) -> Self {
        CachingTransport { inner, cache }
    }

    /// The cache in use.
    pub fn cache(&self) -> &C {
        &self.cache
    }

    /// The wrapped transport.
    pub fn inner(&self) -> &T {
        &self.inner
    }

    /// Unwrap, returning the transport and the cache.
    pub fn into_parts(self) -> (T, C) {
        (self.inner, self.cache)
    }
}

impl<T: Transport, C: ResponseCache> Transport for CachingTransport<T, C> {
    fn supports(&self, endpoint: &Endpoint) -> bool {
        self.inner.supports(endpoint)
    }

    fn fetch(&self, query: &Query) -> Result<RawResponse> {
        let key = CacheKey::of(query);

        if let Some(cached) = self.cache.get(&key) {
            return Ok(cached.mark_cached());
        }

        let response = self.inner.fetch(query)?;
        self.cache.put(key, response.clone());
        Ok(response)
    }

    fn name(&self) -> String {
        format!("cached({})", self.inner.name())
    }
}

/// The asynchronous counterpart of [`CachingTransport`].
#[cfg(feature = "async")]
#[derive(Debug)]
pub struct AsyncCachingTransport<T, C> {
    inner: T,
    cache: C,
}

#[cfg(feature = "async")]
impl<T, C> AsyncCachingTransport<T, C> {
    /// Wrap a transport with a cache.
    pub fn new(inner: T, cache: C) -> Self {
        AsyncCachingTransport { inner, cache }
    }

    /// The cache in use.
    pub fn cache(&self) -> &C {
        &self.cache
    }

    /// The wrapped transport.
    pub fn inner(&self) -> &T {
        &self.inner
    }

    /// Unwrap, returning the transport and the cache.
    pub fn into_parts(self) -> (T, C) {
        (self.inner, self.cache)
    }
}

#[cfg(feature = "async")]
impl<T: AsyncTransport, C: ResponseCache> AsyncTransport for AsyncCachingTransport<T, C> {
    fn supports(&self, endpoint: &Endpoint) -> bool {
        self.inner.supports(endpoint)
    }

    fn fetch<'a>(&'a self, query: &'a Query) -> BoxFuture<'a, Result<RawResponse>> {
        Box::pin(async move {
            let key = CacheKey::of(query);

            if let Some(cached) = self.cache.get(&key) {
                return Ok(cached.mark_cached());
            }

            let response = self.inner.fetch(query).await?;
            self.cache.put(key, response.clone());
            Ok(response)
        })
    }

    fn name(&self) -> String {
        format!("async-cached({})", self.inner.name())
    }
}

#[cfg(test)]
mod tests {
    use std::time::Duration;

    use super::*;
    use crate::cache::{MemoryCache, NullCache};
    use crate::domain::Tld;
    use crate::error::Error;
    use crate::transport::mock::{MockTransport, Scripted};

    fn query(name: &str) -> Query {
        Query::new(
            Endpoint::whois("whois.example"),
            name,
            Tld::parse("com").unwrap(),
        )
    }

    #[test]
    fn a_repeat_question_does_not_reach_the_network() {
        let inner = MockTransport::new(vec![Scripted::Answer("record".into())]);
        let transport = CachingTransport::new(inner.clone(), MemoryCache::new());

        let first = transport.fetch(&query("example.com")).unwrap();
        let second = transport.fetch(&query("example.com")).unwrap();

        assert_eq!(inner.call_count(), 1);
        assert_eq!(first.text(), second.text());
        assert!(!first.is_cached());
        assert!(
            second.is_cached(),
            "the second answer should be marked cached"
        );
    }

    #[test]
    fn different_names_are_different_entries() {
        let inner = MockTransport::new(vec![
            Scripted::Answer("a".into()),
            Scripted::Answer("b".into()),
        ]);
        let transport = CachingTransport::new(inner.clone(), MemoryCache::new());

        assert_eq!(transport.fetch(&query("a.com")).unwrap().text(), "a");
        assert_eq!(transport.fetch(&query("b.com")).unwrap().text(), "b");
        assert_eq!(inner.call_count(), 2);
    }

    #[test]
    fn failures_are_not_remembered() {
        let inner = MockTransport::new(vec![
            Scripted::Fail(Error::Timeout {
                server: "whois.example".into(),
                elapsed: Duration::ZERO,
            }),
            Scripted::Answer("recovered".into()),
        ]);
        let transport = CachingTransport::new(inner.clone(), MemoryCache::new());

        assert!(transport.fetch(&query("example.com")).is_err());
        // A cached failure would keep answering with the error long after the
        // server came back.
        assert_eq!(
            transport.fetch(&query("example.com")).unwrap().text(),
            "recovered"
        );
        assert_eq!(inner.call_count(), 2);
    }

    #[test]
    fn a_null_cache_disables_caching_without_a_branch() {
        let inner = MockTransport::answering("record");
        let transport = CachingTransport::new(inner.clone(), NullCache);

        transport.fetch(&query("example.com")).unwrap();
        let second = transport.fetch(&query("example.com")).unwrap();

        assert_eq!(inner.call_count(), 2);
        assert!(!second.is_cached());
    }

    #[test]
    fn name_shows_the_wrapping() {
        let transport = CachingTransport::new(MockTransport::answering("x"), NullCache);
        assert_eq!(transport.name(), "cached(mock)");
    }

    #[cfg(feature = "async")]
    #[tokio::test]
    async fn the_async_path_caches_too() {
        let inner = MockTransport::new(vec![Scripted::Answer("record".into())]);
        let transport = AsyncCachingTransport::new(inner.clone(), MemoryCache::new());

        let first = transport.fetch(&query("example.com")).await.unwrap();
        let second = transport.fetch(&query("example.com")).await.unwrap();

        assert_eq!(inner.call_count(), 1);
        assert_eq!(first.text(), second.text());
        assert!(second.is_cached());
    }
}