use std::borrow::Cow;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey<'a>(Cow<'a, str>);
impl<'a> CacheKey<'a> {
pub fn new(value: impl Into<Cow<'a, str>>) -> Self {
Self(value.into())
}
pub fn builder() -> CacheKeyBuilder {
CacheKeyBuilder::new()
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_owned(self) -> CacheKey<'static> {
CacheKey(Cow::Owned(self.0.into_owned()))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CacheKeyBuilder {
segments: Vec<String>,
}
impl CacheKeyBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn from_segment(segment: impl ToString) -> Self {
Self::new().segment(segment)
}
pub fn segment(mut self, segment: impl ToString) -> Self {
self.segments.push(escape_segment(&segment.to_string()));
self
}
pub fn segments<I, S>(mut self, segments: I) -> Self
where
I: IntoIterator<Item = S>,
S: ToString,
{
self.segments.extend(
segments
.into_iter()
.map(|segment| escape_segment(&segment.to_string())),
);
self
}
pub fn entity(self, kind: impl ToString, id: impl ToString) -> Self {
self.segment(kind).segment(id)
}
pub fn tenant(self, id: impl ToString) -> Self {
self.segment("tenant").segment(id)
}
pub fn is_empty(&self) -> bool {
self.segments.is_empty()
}
pub fn build(self) -> CacheKey<'static> {
CacheKey::new(self.build_string())
}
pub fn build_string(self) -> String {
self.segments.join(":")
}
}
impl<'a> From<&'a str> for CacheKey<'a> {
fn from(value: &'a str) -> Self {
Self::new(value)
}
}
impl From<String> for CacheKey<'static> {
fn from(value: String) -> Self {
Self::new(Cow::Owned(value))
}
}
impl fmt::Display for CacheKey<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
pub(crate) fn escape_segment(segment: &str) -> String {
let mut escaped = String::with_capacity(segment.len());
for ch in segment.chars() {
match ch {
'%' => escaped.push_str("%25"),
':' => escaped.push_str("%3A"),
_ => escaped.push(ch),
}
}
escaped
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_builder_new_is_empty() {
let builder = CacheKeyBuilder::new();
assert!(builder.is_empty());
assert_eq!(builder.clone().build_string(), "");
assert_eq!(builder.build().as_str(), "");
}
#[test]
fn key_builder_from_segment_adds_initial_segment() {
let key = CacheKeyBuilder::from_segment("users").build_string();
assert_eq!(key, "users");
}
#[test]
fn key_builder_segment_escapes_colon_and_percent() {
let key = CacheKeyBuilder::new()
.segment("tenant:7")
.segment("percent%value")
.build_string();
assert_eq!(key, "tenant%3A7:percent%25value");
}
#[test]
fn key_builder_segments_preserve_order() {
let key = CacheKeyBuilder::new()
.segments(["tenant", "7", "users"])
.build_string();
assert_eq!(key, "tenant:7:users");
}
#[test]
fn key_builder_entity_and_tenant_append_pairs() {
let key = CacheKeyBuilder::new()
.tenant(7)
.entity("user", 42)
.build_string();
assert_eq!(key, "tenant:7:user:42");
}
#[test]
fn cache_key_builder_constructor_matches_direct_builder() {
let key = CacheKey::builder().entity("user", 42).build();
assert_eq!(key.as_str(), "user:42");
assert_eq!(key.to_string(), "user:42");
}
}