Skip to main content

hydracache_core/
key.rs

1use std::borrow::Cow;
2use std::fmt;
3
4/// A logical cache key.
5///
6/// v0 treats keys as application-provided strings. Query adapters may later derive
7/// these keys from SQL text and typed arguments.
8///
9/// # Example
10///
11/// ```rust
12/// use hydracache_core::CacheKey;
13///
14/// let key = CacheKey::new("users:42");
15/// assert_eq!(key.as_str(), "users:42");
16/// ```
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub struct CacheKey<'a>(Cow<'a, str>);
19
20impl<'a> CacheKey<'a> {
21    /// Create a new cache key.
22    pub fn new(value: impl Into<Cow<'a, str>>) -> Self {
23        Self(value.into())
24    }
25
26    /// Start building an owned cache key from escaped segments.
27    ///
28    /// # Example
29    ///
30    /// ```rust
31    /// use hydracache_core::CacheKey;
32    ///
33    /// let key = CacheKey::builder()
34    ///     .segment("tenant:7")
35    ///     .segment("users")
36    ///     .segment(42)
37    ///     .build();
38    ///
39    /// assert_eq!(key.as_str(), "tenant%3A7:users:42");
40    /// ```
41    pub fn builder() -> CacheKeyBuilder {
42        CacheKeyBuilder::new()
43    }
44
45    /// Return the string representation of the key.
46    pub fn as_str(&self) -> &str {
47        &self.0
48    }
49
50    /// Convert this key into an owned key.
51    pub fn into_owned(self) -> CacheKey<'static> {
52        CacheKey(Cow::Owned(self.0.into_owned()))
53    }
54}
55
56/// Builder for cache keys made of escaped `:`-separated segments.
57///
58/// `segment` escapes `:` and `%`, which keeps a single logical segment from
59/// being confused with multiple key segments.
60///
61/// # Example
62///
63/// ```rust
64/// use hydracache_core::CacheKeyBuilder;
65///
66/// let key = CacheKeyBuilder::new()
67///     .segment("tenant")
68///     .segment(7)
69///     .entity("user", 42)
70///     .build_string();
71///
72/// assert_eq!(key, "tenant:7:user:42");
73/// ```
74#[derive(Debug, Clone, Default, PartialEq, Eq)]
75pub struct CacheKeyBuilder {
76    segments: Vec<String>,
77}
78
79impl CacheKeyBuilder {
80    /// Create an empty key builder.
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    /// Create a key builder with one initial segment.
86    pub fn from_segment(segment: impl ToString) -> Self {
87        Self::new().segment(segment)
88    }
89
90    /// Append one escaped key segment.
91    pub fn segment(mut self, segment: impl ToString) -> Self {
92        self.segments.push(escape_segment(&segment.to_string()));
93        self
94    }
95
96    /// Append multiple escaped key segments.
97    pub fn segments<I, S>(mut self, segments: I) -> Self
98    where
99        I: IntoIterator<Item = S>,
100        S: ToString,
101    {
102        self.segments.extend(
103            segments
104                .into_iter()
105                .map(|segment| escape_segment(&segment.to_string())),
106        );
107        self
108    }
109
110    /// Append an escaped entity kind and id pair.
111    pub fn entity(self, kind: impl ToString, id: impl ToString) -> Self {
112        self.segment(kind).segment(id)
113    }
114
115    /// Append a `tenant:{id}` prefix.
116    pub fn tenant(self, id: impl ToString) -> Self {
117        self.segment("tenant").segment(id)
118    }
119
120    /// Return whether no segments have been added.
121    pub fn is_empty(&self) -> bool {
122        self.segments.is_empty()
123    }
124
125    /// Build an owned [`CacheKey`].
126    pub fn build(self) -> CacheKey<'static> {
127        CacheKey::new(self.build_string())
128    }
129
130    /// Build an owned key string.
131    pub fn build_string(self) -> String {
132        self.segments.join(":")
133    }
134}
135
136impl<'a> From<&'a str> for CacheKey<'a> {
137    fn from(value: &'a str) -> Self {
138        Self::new(value)
139    }
140}
141
142impl From<String> for CacheKey<'static> {
143    fn from(value: String) -> Self {
144        Self::new(Cow::Owned(value))
145    }
146}
147
148impl fmt::Display for CacheKey<'_> {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        f.write_str(self.as_str())
151    }
152}
153
154pub(crate) fn escape_segment(segment: &str) -> String {
155    let mut escaped = String::with_capacity(segment.len());
156    for ch in segment.chars() {
157        match ch {
158            '%' => escaped.push_str("%25"),
159            ':' => escaped.push_str("%3A"),
160            _ => escaped.push(ch),
161        }
162    }
163    escaped
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn key_builder_new_is_empty() {
172        let builder = CacheKeyBuilder::new();
173
174        assert!(builder.is_empty());
175        assert_eq!(builder.clone().build_string(), "");
176        assert_eq!(builder.build().as_str(), "");
177    }
178
179    #[test]
180    fn key_builder_from_segment_adds_initial_segment() {
181        let key = CacheKeyBuilder::from_segment("users").build_string();
182
183        assert_eq!(key, "users");
184    }
185
186    #[test]
187    fn key_builder_segment_escapes_colon_and_percent() {
188        let key = CacheKeyBuilder::new()
189            .segment("tenant:7")
190            .segment("percent%value")
191            .build_string();
192
193        assert_eq!(key, "tenant%3A7:percent%25value");
194    }
195
196    #[test]
197    fn key_builder_segments_preserve_order() {
198        let key = CacheKeyBuilder::new()
199            .segments(["tenant", "7", "users"])
200            .build_string();
201
202        assert_eq!(key, "tenant:7:users");
203    }
204
205    #[test]
206    fn key_builder_entity_and_tenant_append_pairs() {
207        let key = CacheKeyBuilder::new()
208            .tenant(7)
209            .entity("user", 42)
210            .build_string();
211
212        assert_eq!(key, "tenant:7:user:42");
213    }
214
215    #[test]
216    fn cache_key_builder_constructor_matches_direct_builder() {
217        let key = CacheKey::builder().entity("user", 42).build();
218
219        assert_eq!(key.as_str(), "user:42");
220        assert_eq!(key.to_string(), "user:42");
221    }
222
223    #[test]
224    fn cache_key_conversions_preserve_owned_and_borrowed_values() {
225        let borrowed = CacheKey::from("users:1");
226        let owned = CacheKey::from(String::from("users:2"));
227        let promoted = borrowed.clone().into_owned();
228
229        assert_eq!(borrowed.as_str(), "users:1");
230        assert_eq!(owned.as_str(), "users:2");
231        assert_eq!(promoted.as_str(), "users:1");
232    }
233}