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};
#[derive(Debug)]
pub struct CachingTransport<T, C> {
inner: T,
cache: C,
}
impl<T, C> CachingTransport<T, C> {
pub fn new(inner: T, cache: C) -> Self {
CachingTransport { inner, cache }
}
pub fn cache(&self) -> &C {
&self.cache
}
pub fn inner(&self) -> &T {
&self.inner
}
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())
}
}
#[cfg(feature = "async")]
#[derive(Debug)]
pub struct AsyncCachingTransport<T, C> {
inner: T,
cache: C,
}
#[cfg(feature = "async")]
impl<T, C> AsyncCachingTransport<T, C> {
pub fn new(inner: T, cache: C) -> Self {
AsyncCachingTransport { inner, cache }
}
pub fn cache(&self) -> &C {
&self.cache
}
pub fn inner(&self) -> &T {
&self.inner
}
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());
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());
}
}