use fastly_shared::FastlyStatus;
use sha2::{Digest, Sha256};
use crate::http::purge::purge_surrogate_key;
use crate::Body;
pub use super::core::CacheKey;
use super::core::{self, Transaction};
use std::fmt::Write as _;
use std::time::Duration;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CacheError {
#[error("Simple Cache operation failed due to a limit")]
LimitExceeded,
#[error("invalid Simple Cache operation; please report this as a bug")]
InvalidOperation,
#[error("unsupported Simple Cache operation")]
Unsupported,
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("purging error: {0}")]
Purge(#[source] crate::Error),
#[error("get_or_set closure error: {0}")]
GetOrSet(#[source] anyhow::Error),
#[error("unknown Simple Cache operation error; please report this as a bug: {0:?}")]
Other(FastlyStatus),
}
impl From<core::CacheError> for CacheError {
fn from(value: core::CacheError) -> Self {
match value {
core::CacheError::LimitExceeded => Self::LimitExceeded,
core::CacheError::InvalidOperation => Self::InvalidOperation,
core::CacheError::Unsupported => Self::Unsupported,
core::CacheError::Other(st) => Self::Other(st),
}
}
}
#[doc = include_str!("../../docs/snippets/key-argument.md")]
pub fn get(key: impl Into<CacheKey>) -> Result<Option<Body>, CacheError> {
let Some(found) = core::lookup(key.into()).execute()? else {
return Ok(None);
};
Ok(Some(found.to_stream()?))
}
#[doc = include_str!("../../docs/snippets/key-body-argument.md")]
pub fn get_or_set(
key: impl Into<CacheKey>,
value: impl Into<Body>,
ttl: Duration,
) -> Result<Body, CacheError> {
get_or_set_with(key, || {
Ok(CacheEntry {
value: value.into(),
ttl,
})
})
.map(|opt| opt.expect("provided closure is infallible"))
}
#[derive(Debug)]
pub struct CacheEntry {
#[doc = include_str!("../../docs/snippets/body-argument.md")]
pub value: Body,
pub ttl: Duration,
}
#[doc = include_str!("../../docs/snippets/key-argument.md")]
pub fn get_or_set_with<F>(
key: impl Into<CacheKey>,
make_entry: F,
) -> Result<Option<Body>, CacheError>
where
F: FnOnce() -> Result<CacheEntry, anyhow::Error>,
{
let key = key.into();
let lookup_tx = Transaction::lookup(key.clone()).execute()?;
if !lookup_tx.must_insert_or_update() {
if let Some(found) = lookup_tx.found() {
return Ok(Some(found.to_stream()?));
} else {
return Err(CacheError::InvalidOperation);
}
}
let CacheEntry { value, ttl } = make_entry().map_err(CacheError::GetOrSet)?;
let (mut insert_body, found) = lookup_tx
.insert(ttl)
.surrogate_keys([
surrogate_key_for_cache_key(&key, PurgeScope::Pop).as_str(),
surrogate_key_for_cache_key(&key, PurgeScope::Global).as_str(),
])
.execute_and_stream_back()?;
insert_body.append(value);
insert_body.finish()?;
Ok(Some(found.to_stream()?))
}
#[doc = include_str!("../../docs/snippets/key-body-argument.md")]
#[allow(unused)]
fn set(key: impl Into<CacheKey>, value: impl Into<Body>, ttl: Duration) -> Result<(), CacheError> {
let key = key.into();
let mut insert_body = core::insert(key.clone(), ttl)
.surrogate_keys([
surrogate_key_for_cache_key(&key, PurgeScope::Pop).as_str(),
surrogate_key_for_cache_key(&key, PurgeScope::Global).as_str(),
])
.execute()?;
insert_body.append(value.into());
Ok(insert_body.finish()?)
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
enum PurgeScope {
#[default]
Pop,
Global,
}
#[derive(Copy, Clone, Debug, Default)]
pub struct PurgeOptions {
scope: PurgeScope,
}
impl PurgeOptions {
pub fn pop_scope() -> Self {
Self {
scope: PurgeScope::Pop,
}
}
#[doc = include_str!("../../docs/snippets/global-purge.md")]
pub fn global_scope() -> Self {
Self {
scope: PurgeScope::Global,
}
}
}
#[doc = include_str!("../../docs/snippets/key-argument.md")]
pub fn purge(key: impl Into<CacheKey>) -> Result<(), CacheError> {
purge_surrogate_key(&surrogate_key_for_cache_key(
&key.into(),
PurgeOptions::default().scope,
))
.map_err(CacheError::Purge)
}
#[doc = include_str!("../../docs/snippets/key-argument.md")]
#[doc = include_str!("../../docs/snippets/global-purge.md")]
pub fn purge_with_opts(key: impl Into<CacheKey>, opts: PurgeOptions) -> Result<(), CacheError> {
purge_surrogate_key(&surrogate_key_for_cache_key(&key.into(), opts.scope))
.map_err(CacheError::Purge)
}
fn surrogate_key_for_cache_key(key: &CacheKey, scope: PurgeScope) -> String {
let mut sha = Sha256::new();
sha.update(key);
if let PurgeScope::Pop = scope {
let pop = crate::compute_runtime::pop();
sha.update(pop);
}
let mut sk_str = String::new();
for b in sha.finalize() {
write!(&mut sk_str, "{b:02X}").expect("writing to a String is infallible");
}
sk_str
}