1use std::time::Duration;
2
3use serde::{de::DeserializeOwned, Serialize};
4
5use crate::backend::Backend;
6use crate::error::CachekitError;
7use crate::serializer;
8
9#[cfg(not(any(target_arch = "wasm32", feature = "unsync")))]
17pub type SharedBackend = std::sync::Arc<dyn Backend>;
18
19#[cfg(any(target_arch = "wasm32", feature = "unsync"))]
21pub type SharedBackend = std::rc::Rc<dyn Backend>;
22
23#[cfg(all(
31 feature = "encryption",
32 not(any(target_arch = "wasm32", feature = "unsync"))
33))]
34type SharedEncryption = std::sync::Arc<crate::encryption::EncryptionLayer>;
35
36#[cfg(all(
37 feature = "encryption",
38 any(target_arch = "wasm32", feature = "unsync")
39))]
40type SharedEncryption = std::rc::Rc<crate::encryption::EncryptionLayer>;
41
42const MAX_KEY_BYTES: usize = 1024;
45
46const L1_BACKFILL_TTL_SECS: u64 = 30;
49
50fn validate_key(key: &str) -> Result<(), CachekitError> {
51 if key.is_empty() {
52 return Err(CachekitError::InvalidKey(
53 "key must not be empty".to_owned(),
54 ));
55 }
56 if key.len() > MAX_KEY_BYTES {
57 return Err(CachekitError::InvalidKey(format!(
58 "key is {} bytes (limit: {MAX_KEY_BYTES})",
59 key.len()
60 )));
61 }
62 for b in key.bytes() {
63 if b < 0x20 || b == 0x7F {
64 return Err(CachekitError::InvalidKey(format!(
65 "key contains illegal control character 0x{b:02X}"
66 )));
67 }
68 }
69 Ok(())
70}
71
72pub struct CacheKit {
76 backend: SharedBackend,
77 default_ttl: Duration,
78 namespace: Option<String>,
79 max_payload_bytes: usize,
80
81 #[cfg(feature = "l1")]
82 l1: Option<crate::l1::L1Cache>,
83
84 #[cfg(feature = "encryption")]
85 encryption: Option<SharedEncryption>,
86}
87
88impl CacheKit {
89 pub fn builder() -> CacheKitBuilder {
91 CacheKitBuilder::default()
92 }
93
94 #[cfg(all(feature = "cachekitio", not(target_arch = "wasm32")))]
99 pub fn from_env() -> Result<CacheKitBuilder, CachekitError> {
100 use crate::backend::cachekitio::CachekitIO;
101 use crate::config::CachekitConfig;
102
103 let config = CachekitConfig::from_env()?;
104
105 let api_key_z = config
106 .api_key
107 .ok_or_else(|| CachekitError::Config("CACHEKIT_API_KEY is required".to_owned()))?;
108
109 let backend = CachekitIO::builder()
110 .api_key(api_key_z.as_str())
111 .api_url(config.api_url)
112 .build()
113 .map_err(|e| CachekitError::Config(e.to_string()))?;
114
115 #[cfg(not(feature = "unsync"))]
116 let shared: SharedBackend = std::sync::Arc::new(backend);
117 #[cfg(feature = "unsync")]
118 let shared: SharedBackend = std::rc::Rc::new(backend);
119
120 let mut builder = CacheKitBuilder::default()
121 .backend(shared)
122 .default_ttl(config.default_ttl)
123 .max_payload_bytes(config.max_payload_bytes)
124 .l1_capacity(config.l1_capacity);
125
126 if let Some(ns) = config.namespace.clone() {
127 builder = builder.namespace(ns);
128 }
129
130 #[cfg(feature = "encryption")]
132 if let Some(ref master_key) = config.master_key {
133 let namespace = config.namespace.as_deref().unwrap_or("default");
134 builder = builder.encryption_from_bytes(master_key, namespace)?;
135 }
136
137 Ok(builder)
138 }
139
140 fn namespaced_key(&self, key: &str) -> String {
143 match &self.namespace {
144 Some(ns) => format!("{ns}:{key}"),
145 None => key.to_owned(),
146 }
147 }
148
149 fn resolve_key(&self, key: &str) -> Result<String, CachekitError> {
151 validate_key(key)?;
152 Ok(self.namespaced_key(key))
153 }
154
155 #[cfg(feature = "l1")]
159 fn l1_get(&self, full_key: &str) -> Option<Vec<u8>> {
160 self.l1.as_ref().and_then(|l1| l1.get(full_key))
161 }
162
163 #[cfg(feature = "l1")]
165 fn l1_backfill(&self, full_key: &str, bytes: &[u8]) {
166 if let Some(ref l1) = self.l1 {
167 let l1_ttl = std::cmp::min(self.default_ttl, Duration::from_secs(L1_BACKFILL_TTL_SECS));
168 l1.set(full_key, bytes, l1_ttl);
169 }
170 }
171
172 #[cfg(feature = "l1")]
174 fn l1_set(&self, full_key: &str, bytes: &[u8], ttl: Duration) {
175 if let Some(ref l1) = self.l1 {
176 l1.set(full_key, bytes, ttl);
177 }
178 }
179
180 #[cfg(feature = "l1")]
182 fn l1_delete(&self, full_key: &str) {
183 if let Some(ref l1) = self.l1 {
184 l1.delete(full_key);
185 }
186 }
187
188 fn validate_ttl(ttl: Duration) -> Result<(), CachekitError> {
190 if ttl < Duration::from_secs(1) {
191 return Err(CachekitError::Config(format!(
192 "TTL must be at least 1 second; got {ttl:?}"
193 )));
194 }
195 Ok(())
196 }
197
198 pub async fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, CachekitError> {
205 match self.get_bytes(key).await? {
206 Some(bytes) => Ok(Some(serializer::deserialize(&bytes)?)),
207 None => Ok(None),
208 }
209 }
210
211 pub async fn interop_get<T: DeserializeOwned>(
233 &self,
234 key: &str,
235 ) -> Result<Option<T>, CachekitError> {
236 self.reject_namespaced_interop()?;
237 match self.get_bytes(key).await? {
238 Some(bytes) => Ok(Some(crate::interop::deserialize(&bytes)?)),
239 None => Ok(None),
240 }
241 }
242
243 fn reject_namespaced_interop(&self) -> Result<(), CachekitError> {
246 match self.namespace {
247 None => Ok(()),
248 Some(_) => Err(CachekitError::Config(
249 "interop reads require a client without a namespace prefix: .namespace() / \
250 CACHEKIT_NAMESPACE would store interop entries under {prefix}:{interop_key}, \
251 which other SDKs never compute (interop keys already carry a namespace \
252 segment) — use a dedicated non-namespaced client for interop entries"
253 .to_owned(),
254 )),
255 }
256 }
257
258 async fn get_bytes(&self, key: &str) -> Result<Option<Vec<u8>>, CachekitError> {
260 let full_key = self.resolve_key(key)?;
261
262 #[cfg(feature = "l1")]
264 if let Some(bytes) = self.l1_get(&full_key) {
265 self.check_payload_size(bytes.len())?;
266 return Ok(Some(bytes));
267 }
268
269 let bytes = match self.backend.get(&full_key).await? {
271 Some(b) => b,
272 None => return Ok(None),
273 };
274
275 self.check_payload_size(bytes.len())?;
276
277 #[cfg(feature = "l1")]
279 self.l1_backfill(&full_key, &bytes);
280
281 Ok(Some(bytes))
282 }
283
284 pub async fn set<T: Serialize>(&self, key: &str, value: &T) -> Result<(), CachekitError> {
286 self.set_with_ttl(key, value, self.default_ttl).await
287 }
288
289 pub async fn set_with_ttl<T: Serialize>(
293 &self,
294 key: &str,
295 value: &T,
296 ttl: Duration,
297 ) -> Result<(), CachekitError> {
298 Self::validate_ttl(ttl)?;
299
300 let bytes = serializer::serialize(value)?;
301 self.check_payload_size(bytes.len())?;
302
303 let full_key = self.resolve_key(key)?;
304
305 #[cfg(feature = "l1")]
307 {
308 let l1_bytes = bytes.clone();
309 self.backend.set(&full_key, bytes, Some(ttl)).await?;
310 self.l1_set(&full_key, &l1_bytes, ttl);
311 }
312 #[cfg(not(feature = "l1"))]
313 {
314 self.backend.set(&full_key, bytes, Some(ttl)).await?;
315 }
316
317 Ok(())
318 }
319
320 pub async fn delete(&self, key: &str) -> Result<bool, CachekitError> {
324 let full_key = self.resolve_key(key)?;
325
326 #[cfg(feature = "l1")]
329 self.l1_delete(&full_key);
330
331 Ok(self.backend.delete(&full_key).await?)
332 }
333
334 pub async fn exists(&self, key: &str) -> Result<bool, CachekitError> {
336 let full_key = self.resolve_key(key)?;
337
338 #[cfg(feature = "l1")]
340 if self.l1_get(&full_key).is_some() {
341 return Ok(true);
342 }
343
344 Ok(self.backend.exists(&full_key).await?)
345 }
346
347 #[cfg(feature = "encryption")]
359 pub fn secure(&self) -> Result<SecureCache<'_>, CachekitError> {
360 let enc = self.encryption.as_ref().ok_or_else(|| {
361 CachekitError::Config(
362 "encryption requires CACHEKIT_MASTER_KEY or .encryption() on builder".to_owned(),
363 )
364 })?;
365 Ok(SecureCache {
366 client: self,
367 encryption: enc,
368 })
369 }
370
371 fn check_payload_size(&self, size: usize) -> Result<(), CachekitError> {
374 if size > self.max_payload_bytes {
375 return Err(CachekitError::PayloadTooLarge {
376 size,
377 limit: self.max_payload_bytes,
378 });
379 }
380 Ok(())
381 }
382}
383
384#[cfg(feature = "encryption")]
391pub struct SecureCache<'a> {
392 client: &'a CacheKit,
393 encryption: &'a crate::encryption::EncryptionLayer,
394}
395
396#[cfg(feature = "encryption")]
397impl std::fmt::Debug for SecureCache<'_> {
398 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399 f.debug_struct("SecureCache")
400 .field("tenant_id", &self.encryption.tenant_id())
401 .finish()
402 }
403}
404
405#[cfg(feature = "encryption")]
406impl SecureCache<'_> {
407 pub async fn set<T: Serialize>(&self, key: &str, value: &T) -> Result<(), CachekitError> {
409 self.set_with_ttl(key, value, self.client.default_ttl).await
410 }
411
412 pub async fn set_with_ttl<T: Serialize>(
414 &self,
415 key: &str,
416 value: &T,
417 ttl: Duration,
418 ) -> Result<(), CachekitError> {
419 CacheKit::validate_ttl(ttl)?;
420
421 let plaintext = serializer::serialize(value)?;
423 let ciphertext = self.encryption.encrypt(&plaintext, key)?;
424 self.client.check_payload_size(ciphertext.len())?;
429
430 let full_key = self.client.resolve_key(key)?;
431
432 #[cfg(feature = "l1")]
434 {
435 let l1_bytes = ciphertext.clone();
436 self.client
437 .backend
438 .set(&full_key, ciphertext, Some(ttl))
439 .await?;
440 self.client.l1_set(&full_key, &l1_bytes, ttl);
441 }
442 #[cfg(not(feature = "l1"))]
443 {
444 self.client
445 .backend
446 .set(&full_key, ciphertext, Some(ttl))
447 .await?;
448 }
449
450 Ok(())
451 }
452
453 pub async fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, CachekitError> {
457 match self.get_plaintext(key).await? {
458 Some(plaintext) => Ok(Some(serializer::deserialize(&plaintext)?)),
459 None => Ok(None),
460 }
461 }
462
463 pub async fn interop_get<T: DeserializeOwned>(
477 &self,
478 key: &str,
479 ) -> Result<Option<T>, CachekitError> {
480 self.client.reject_namespaced_interop()?;
481 match self.get_plaintext(key).await? {
482 Some(plaintext) => Ok(Some(crate::interop::deserialize(&plaintext)?)),
483 None => Ok(None),
484 }
485 }
486
487 async fn get_plaintext(&self, key: &str) -> Result<Option<Vec<u8>>, CachekitError> {
493 match self.client.get_bytes(key).await? {
494 Some(ciphertext) => Ok(Some(self.encryption.decrypt(&ciphertext, key)?)),
495 None => Ok(None),
496 }
497 }
498
499 pub async fn delete(&self, key: &str) -> Result<bool, CachekitError> {
501 self.client.delete(key).await
502 }
503
504 pub async fn exists(&self, key: &str) -> Result<bool, CachekitError> {
506 self.client.exists(key).await
507 }
508}
509
510#[derive(Default)]
514#[must_use]
515pub struct CacheKitBuilder {
516 backend: Option<SharedBackend>,
517 default_ttl: Option<Duration>,
518 namespace: Option<String>,
519 max_payload_bytes: Option<usize>,
520
521 #[cfg(feature = "l1")]
522 l1_capacity: Option<usize>,
523
524 #[cfg(feature = "l1")]
525 no_l1: bool,
526
527 #[cfg(feature = "encryption")]
528 encryption: Option<SharedEncryption>,
529}
530
531impl CacheKitBuilder {
532 pub fn backend(mut self, backend: SharedBackend) -> Self {
534 self.backend = Some(backend);
535 self
536 }
537
538 pub fn default_ttl(mut self, ttl: Duration) -> Self {
540 self.default_ttl = Some(ttl);
541 self
542 }
543
544 pub fn namespace(mut self, ns: impl Into<String>) -> Self {
546 self.namespace = Some(ns.into());
547 self
548 }
549
550 pub fn max_payload_bytes(mut self, limit: usize) -> Self {
552 self.max_payload_bytes = Some(limit);
553 self
554 }
555
556 #[cfg(feature = "l1")]
558 pub fn l1_capacity(mut self, capacity: usize) -> Self {
559 self.l1_capacity = Some(capacity);
560 self
561 }
562
563 #[cfg(feature = "l1")]
565 pub fn no_l1(mut self) -> Self {
566 self.no_l1 = true;
567 self
568 }
569
570 #[cfg(not(feature = "l1"))]
572 pub fn l1_capacity(self, _capacity: usize) -> Self {
573 self
574 }
575
576 #[cfg(not(feature = "l1"))]
577 pub fn no_l1(self) -> Self {
578 self
579 }
580
581 #[cfg(feature = "encryption")]
586 pub fn encryption_from_bytes(
587 mut self,
588 master_key: &[u8],
589 tenant_id: &str,
590 ) -> Result<Self, CachekitError> {
591 let layer = crate::encryption::EncryptionLayer::new(master_key, tenant_id)?;
592 self.encryption = Some(SharedEncryption::new(layer));
593 Ok(self)
594 }
595
596 #[cfg(feature = "encryption")]
601 pub fn encryption(self, hex_key: &str, tenant_id: &str) -> Result<Self, CachekitError> {
602 let bytes = hex::decode(hex_key)
603 .map_err(|e| CachekitError::Config(format!("master key is not valid hex: {e}")))?;
604 self.encryption_from_bytes(&bytes, tenant_id)
605 }
606
607 #[cfg(not(feature = "encryption"))]
609 pub fn encryption_from_bytes(
610 self,
611 _master_key: &[u8],
612 _tenant_id: &str,
613 ) -> Result<Self, CachekitError> {
614 Ok(self)
615 }
616
617 #[cfg(not(feature = "encryption"))]
618 pub fn encryption(self, _hex_key: &str, _tenant_id: &str) -> Result<Self, CachekitError> {
619 Ok(self)
620 }
621
622 pub fn build(self) -> Result<CacheKit, CachekitError> {
626 let backend = self.backend.ok_or_else(|| {
627 CachekitError::Config("a backend must be provided via .backend()".to_owned())
628 })?;
629
630 if let Some(ref ns) = self.namespace {
632 if ns.is_empty() {
633 return Err(CachekitError::Config("namespace cannot be empty".into()));
634 }
635 if ns.len() > 255 {
636 return Err(CachekitError::Config("namespace exceeds 255 bytes".into()));
637 }
638 if !ns.bytes().all(|b| (0x20..=0x7E).contains(&b)) {
639 return Err(CachekitError::Config(
640 "namespace must be ASCII printable".into(),
641 ));
642 }
643 }
644
645 #[cfg(feature = "l1")]
646 let l1 = if self.no_l1 {
647 None
648 } else {
649 let capacity = self.l1_capacity.unwrap_or(1000);
650 Some(crate::l1::L1Cache::new(capacity))
651 };
652
653 Ok(CacheKit {
654 backend,
655 default_ttl: self.default_ttl.unwrap_or(Duration::from_secs(300)),
656 namespace: self.namespace,
657 max_payload_bytes: self.max_payload_bytes.unwrap_or(5 * 1024 * 1024),
658
659 #[cfg(feature = "l1")]
660 l1,
661
662 #[cfg(feature = "encryption")]
663 encryption: self.encryption,
664 })
665 }
666}