http_cache_darkbird/
lib.rs

1mod error;
2
3use http_cache::{CacheManager, HttpResponse, Result};
4
5use std::{fmt, sync::Arc, time::SystemTime};
6
7use darkbird::{
8    document::{self, RangeField},
9    Options, Storage, StorageType,
10};
11use http_cache_semantics::CachePolicy;
12use serde::{Deserialize, Serialize};
13
14/// Implements [`CacheManager`] with [`darkbird`](https://github.com/Rustixir/darkbird) as the backend.
15#[derive(Clone)]
16pub struct DarkbirdManager {
17    /// The instance of `darkbird::Storage<String, Store>`
18    pub cache: Arc<Storage<String, Store>>,
19    /// Whether full text search should be enabled
20    pub full_text: bool,
21}
22
23impl fmt::Debug for DarkbirdManager {
24    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
25        // need to add more data, anything helpful
26        f.debug_struct("DarkbirdManager").finish_non_exhaustive()
27    }
28}
29
30/// The data stored in the cache
31#[derive(Clone, Debug, Deserialize, Serialize)]
32pub struct Store {
33    /// The HTTP response
34    pub response: HttpResponse,
35    /// The cache policy generated for the response
36    pub policy: CachePolicy,
37    /// The cache key for this entry
38    pub cache_key: String,
39    full_text: bool,
40}
41
42impl document::Document for Store {}
43
44impl document::Indexer for Store {
45    fn extract(&self) -> Vec<String> {
46        vec![self.cache_key.clone()]
47    }
48}
49
50impl document::Tags for Store {
51    fn get_tags(&self) -> Vec<String> {
52        vec![self.response.url.to_string()]
53    }
54}
55
56impl document::Range for Store {
57    fn get_fields(&self) -> Vec<RangeField> {
58        vec![
59            RangeField {
60                name: String::from("age"),
61                value: self.policy.age(SystemTime::now()).as_secs().to_string(),
62            },
63            RangeField {
64                name: String::from("time_to_live"),
65                value: self
66                    .policy
67                    .time_to_live(SystemTime::now())
68                    .as_secs()
69                    .to_string(),
70            },
71        ]
72    }
73}
74
75impl document::MaterializedView for Store {
76    fn filter(&self) -> Option<String> {
77        if self.policy.is_stale(SystemTime::now()) {
78            Some(String::from("stale"))
79        } else {
80            None
81        }
82    }
83}
84
85impl document::FullText for Store {
86    fn get_content(&self) -> Option<String> {
87        if self.full_text {
88            Some(String::from_utf8_lossy(&self.response.body).to_string())
89        } else {
90            None
91        }
92    }
93}
94
95impl DarkbirdManager {
96    /// Create a new manager with provided options
97    pub async fn new(options: Options<'_>, full_text: bool) -> Result<Self> {
98        Ok(Self {
99            cache: Arc::new(Storage::<String, Store>::open(options).await?),
100            full_text,
101        })
102    }
103
104    /// Create a new manager with default options
105    pub async fn new_with_defaults() -> Result<Self> {
106        let ops = Options::new(
107            ".",
108            "http-darkbird",
109            42,
110            StorageType::RamCopies,
111            true,
112        );
113        Self::new(ops, false).await
114    }
115}
116
117#[async_trait::async_trait]
118impl CacheManager for DarkbirdManager {
119    async fn get(
120        &self,
121        cache_key: &str,
122    ) -> Result<Option<(HttpResponse, CachePolicy)>> {
123        let store: Store = match self.cache.lookup(&cache_key.to_string()) {
124            Some(d) => d.value().clone(),
125            None => return Ok(None),
126        };
127        Ok(Some((store.response, store.policy)))
128    }
129
130    async fn put(
131        &self,
132        cache_key: String,
133        response: HttpResponse,
134        policy: CachePolicy,
135    ) -> Result<HttpResponse> {
136        let data = Store {
137            response: response.clone(),
138            policy,
139            cache_key: cache_key.clone(),
140            full_text: self.full_text,
141        };
142        let mut exists = false;
143        if self.cache.lookup(&cache_key.to_string()).is_some() {
144            exists = true;
145        }
146        if exists {
147            self.delete(&cache_key).await?;
148        }
149        match self.cache.insert(cache_key, data).await {
150            Ok(_) => {}
151            Err(e) => {
152                return Err(Box::new(error::Error::Put(e.to_string())));
153            }
154        };
155        Ok(response)
156    }
157
158    async fn delete(&self, cache_key: &str) -> Result<()> {
159        match self.cache.remove(cache_key.to_string()).await {
160            Ok(_) => {}
161            Err(e) => {
162                return Err(Box::new(error::Error::Delete(e.to_string())));
163            }
164        };
165        Ok(())
166    }
167}
168
169#[cfg(test)]
170mod test;