1use std::borrow::Cow;
2use std::fmt;
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub struct CacheKey<'a>(Cow<'a, str>);
19
20impl<'a> CacheKey<'a> {
21 pub fn new(value: impl Into<Cow<'a, str>>) -> Self {
23 Self(value.into())
24 }
25
26 pub fn builder() -> CacheKeyBuilder {
42 CacheKeyBuilder::new()
43 }
44
45 pub fn as_str(&self) -> &str {
47 &self.0
48 }
49
50 pub fn into_owned(self) -> CacheKey<'static> {
52 CacheKey(Cow::Owned(self.0.into_owned()))
53 }
54}
55
56#[derive(Debug, Clone, Default, PartialEq, Eq)]
75pub struct CacheKeyBuilder {
76 segments: Vec<String>,
77}
78
79impl CacheKeyBuilder {
80 pub fn new() -> Self {
82 Self::default()
83 }
84
85 pub fn from_segment(segment: impl ToString) -> Self {
87 Self::new().segment(segment)
88 }
89
90 pub fn segment(mut self, segment: impl ToString) -> Self {
92 self.segments.push(escape_segment(&segment.to_string()));
93 self
94 }
95
96 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 pub fn entity(self, kind: impl ToString, id: impl ToString) -> Self {
112 self.segment(kind).segment(id)
113 }
114
115 pub fn tenant(self, id: impl ToString) -> Self {
117 self.segment("tenant").segment(id)
118 }
119
120 pub fn is_empty(&self) -> bool {
122 self.segments.is_empty()
123 }
124
125 pub fn build(self) -> CacheKey<'static> {
127 CacheKey::new(self.build_string())
128 }
129
130 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}