1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5use std::error::Error;
6use std::time::{Duration, SystemTime};
7
8macro_rules! string_newtype {
9 ($(#[$meta:meta])* $name:ident) => {
10 $(#[$meta])*
11 #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
12 pub struct $name(String);
13
14 impl $name {
15 pub fn new(value: impl Into<String>) -> Self {
17 Self(value.into())
18 }
19
20 pub fn as_str(&self) -> &str {
22 &self.0
23 }
24 }
25
26 impl AsRef<str> for $name {
27 fn as_ref(&self) -> &str {
28 self.as_str()
29 }
30 }
31
32 impl From<String> for $name {
33 fn from(value: String) -> Self {
34 Self::new(value)
35 }
36 }
37
38 impl From<&str> for $name {
39 fn from(value: &str) -> Self {
40 Self::new(value)
41 }
42 }
43
44 impl fmt::Display for $name {
45 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46 formatter.write_str(self.as_str())
47 }
48 }
49 };
50}
51
52string_newtype! {
53 CacheKey
55}
56string_newtype! {
57 CacheNamespace
59}
60string_newtype! {
61 CacheValue
63}
64
65impl CacheKey {
66 pub fn builder() -> CacheKeyBuilder {
68 CacheKeyBuilder::new()
69 }
70}
71
72#[derive(Clone, Debug, Default, Eq, PartialEq)]
74pub struct CacheKeyBuilder {
75 namespace: Option<CacheNamespace>,
76 segments: Vec<String>,
77}
78
79impl CacheKeyBuilder {
80 pub const fn new() -> Self {
82 Self {
83 namespace: None,
84 segments: Vec::new(),
85 }
86 }
87
88 pub fn namespace(mut self, namespace: CacheNamespace) -> Self {
90 self.namespace = Some(namespace);
91 self
92 }
93
94 pub fn segment(mut self, segment: impl AsRef<str>) -> Self {
96 let value = segment.as_ref();
97 if !value.is_empty() {
98 self.segments.push(value.to_owned());
99 }
100 self
101 }
102
103 pub fn build(self) -> CacheKey {
105 let mut parts = Vec::new();
106 if let Some(namespace) = self.namespace {
107 parts.push(namespace.to_string());
108 }
109 parts.extend(self.segments);
110 CacheKey::new(parts.join(":"))
111 }
112}
113
114#[derive(Clone, Copy, Debug, Eq, PartialEq)]
116pub enum InvalidTtlError {
117 ZeroDuration,
119}
120
121impl fmt::Display for InvalidTtlError {
122 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123 match self {
124 Self::ZeroDuration => formatter.write_str("ttl duration must be greater than zero"),
125 }
126 }
127}
128
129impl Error for InvalidTtlError {}
130
131#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
133pub struct Ttl(Duration);
134
135impl Ttl {
136 pub fn new(duration: Duration) -> Result<Self, InvalidTtlError> {
138 if duration.is_zero() {
139 Err(InvalidTtlError::ZeroDuration)
140 } else {
141 Ok(Self(duration))
142 }
143 }
144
145 pub fn seconds(seconds: u64) -> Result<Self, InvalidTtlError> {
147 Self::new(Duration::from_secs(seconds))
148 }
149
150 pub const fn duration(self) -> Duration {
152 self.0
153 }
154}
155
156#[derive(Clone, Copy, Debug, Eq, PartialEq)]
158pub enum Expiration {
159 Never,
160 At(SystemTime),
161 After(Ttl),
162}
163
164#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
166pub enum EvictionPolicy {
167 Lru,
168 Lfu,
169 Fifo,
170 Ttl,
171 Manual,
172 #[default]
173 Unknown,
174}
175
176impl EvictionPolicy {
177 pub const fn as_str(self) -> &'static str {
179 match self {
180 Self::Lru => "lru",
181 Self::Lfu => "lfu",
182 Self::Fifo => "fifo",
183 Self::Ttl => "ttl",
184 Self::Manual => "manual",
185 Self::Unknown => "unknown",
186 }
187 }
188}
189
190impl fmt::Display for EvictionPolicy {
191 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
192 formatter.write_str(self.as_str())
193 }
194}
195
196#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
198pub enum CacheStatus {
199 Hit,
200 Miss,
201 Stale,
202 Expired,
203 #[default]
204 Unknown,
205}
206
207impl CacheStatus {
208 pub const fn as_str(self) -> &'static str {
210 match self {
211 Self::Hit => "hit",
212 Self::Miss => "miss",
213 Self::Stale => "stale",
214 Self::Expired => "expired",
215 Self::Unknown => "unknown",
216 }
217 }
218}
219
220impl fmt::Display for CacheStatus {
221 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
222 formatter.write_str(self.as_str())
223 }
224}
225
226#[derive(Clone, Debug, Eq, PartialEq)]
228pub struct CacheEntry {
229 key: CacheKey,
230 value: CacheValue,
231 ttl: Option<Ttl>,
232 status: CacheStatus,
233}
234
235impl CacheEntry {
236 pub const fn new(key: CacheKey, value: CacheValue) -> Self {
238 Self {
239 key,
240 value,
241 ttl: None,
242 status: CacheStatus::Unknown,
243 }
244 }
245
246 pub const fn with_ttl(mut self, ttl: Ttl) -> Self {
248 self.ttl = Some(ttl);
249 self
250 }
251
252 pub const fn with_status(mut self, status: CacheStatus) -> Self {
254 self.status = status;
255 self
256 }
257
258 pub const fn key(&self) -> &CacheKey {
260 &self.key
261 }
262
263 pub const fn value(&self) -> &CacheValue {
265 &self.value
266 }
267
268 pub const fn ttl(&self) -> Option<Ttl> {
270 self.ttl
271 }
272
273 pub const fn status(&self) -> CacheStatus {
275 self.status
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::{
282 CacheEntry, CacheKey, CacheNamespace, CacheStatus, CacheValue, EvictionPolicy,
283 InvalidTtlError, Ttl,
284 };
285
286 #[test]
287 fn composes_cache_keys() {
288 let key = CacheKey::builder()
289 .namespace(CacheNamespace::new("reviews"))
290 .segment("location")
291 .segment("fort-wayne")
292 .segment("summary")
293 .build();
294
295 assert_eq!(key.to_string(), "reviews:location:fort-wayne:summary");
296 }
297
298 #[test]
299 fn validates_ttl_and_builds_entries() -> Result<(), InvalidTtlError> {
300 let ttl = Ttl::seconds(60)?;
301 let entry = CacheEntry::new(CacheKey::new("reviews:summary"), CacheValue::new("cached"))
302 .with_ttl(ttl)
303 .with_status(CacheStatus::Hit);
304
305 assert_eq!(entry.ttl(), Some(ttl));
306 assert_eq!(entry.status(), CacheStatus::Hit);
307 assert_eq!(Ttl::seconds(0), Err(InvalidTtlError::ZeroDuration));
308 assert_eq!(EvictionPolicy::Lru.to_string(), "lru");
309
310 Ok(())
311 }
312}