use std::collections::HashMap;
use async_trait::async_trait;
use platform_core::{preload, AppError, ComposableFunction, EventEnvelope};
use redis_connection::duration_seconds;
use rmpv::Value;
use crate::action::CacheAction;
use crate::runtime;
use crate::store::RedisCacheStore;
pub const CACHE_ROUTE: &str = "v1.cache.redis";
const ACTION: &str = "action";
const KEY: &str = "key";
const TTL: &str = "ttl";
#[preload(
route = "v1.cache.redis",
instances = 20,
env_instances = "redis.cache.instances"
)]
#[optional_service("redis.cache.enabled")]
pub struct RedisCache;
#[async_trait]
impl ComposableFunction for RedisCache {
async fn handle_event(
&self,
headers: HashMap<String, String>,
input: EventEnvelope,
_instance: usize,
) -> Result<EventEnvelope, AppError> {
let store = runtime::store().await?;
handle(&headers, input.body(), &store).await
}
}
pub async fn handle(
headers: &HashMap<String, String>,
input: &Value,
store: &RedisCacheStore,
) -> Result<EventEnvelope, AppError> {
let action = CacheAction::from_header(headers.get(ACTION).map(String::as_str))?;
let key = headers.get(KEY).map(String::as_str);
let reply = match action {
CacheAction::Get => binary_or_nil(store.get(key).await?),
CacheAction::Put => {
store
.put(key, &as_bytes(input)?, ttl(headers, store)?)
.await?;
Value::Boolean(true)
}
CacheAction::Delete => Value::from(store.delete(key).await?),
CacheAction::PutIfNotPresent => Value::Boolean(
store
.put_if_absent(key, &as_bytes(input)?, ttl(headers, store)?)
.await?,
),
CacheAction::Mget => Value::Map(
store
.mget(&as_key_list(input)?)
.await?
.into_iter()
.map(|(key, value)| (Value::from(key), Value::Binary(value)))
.collect(),
),
CacheAction::Mput => {
store
.mput(&as_entry_map(input)?, ttl(headers, store)?)
.await?;
Value::Boolean(true)
}
CacheAction::ListPush => Value::from(
store
.list_push(key, &as_bytes(input)?, ttl(headers, store)?)
.await?,
),
CacheAction::ListPop => binary_or_nil(store.list_pop(key).await?),
CacheAction::ListLen => Value::from(store.list_len(key).await?),
};
Ok(EventEnvelope::new().set_raw_body(reply))
}
fn binary_or_nil(value: Option<Vec<u8>>) -> Value {
value.map(Value::Binary).unwrap_or(Value::Nil)
}
fn ttl(headers: &HashMap<String, String>, store: &RedisCacheStore) -> Result<u64, AppError> {
match headers.get(TTL).map(|text| text.trim()) {
Some(text) if !text.is_empty() => duration_seconds(text)
.filter(|seconds| *seconds > 0)
.ok_or_else(|| AppError::new(400, format!("Invalid 'ttl' - {text}"))),
_ => Ok(store.default_ttl_seconds()),
}
}
fn as_bytes(input: &Value) -> Result<Vec<u8>, AppError> {
match input {
Value::Binary(bytes) => Ok(bytes.clone()),
Value::String(text) => Ok(text.as_bytes().to_vec()),
_ => Err(AppError::new(
400,
"A value (byte[] or String) is required in the body",
)),
}
}
fn as_key_list(input: &Value) -> Result<Vec<String>, AppError> {
match input {
Value::Array(items) => Ok(items
.iter()
.filter(|item| !item.is_nil())
.map(text_of)
.collect()),
_ => Err(AppError::new(
400,
"MGET requires a List of keys in the body",
)),
}
}
fn as_entry_map(input: &Value) -> Result<Vec<(String, Vec<u8>)>, AppError> {
match input {
Value::Map(entries) => entries
.iter()
.map(|(key, value)| Ok((text_of(key), as_bytes(value)?)))
.collect(),
_ => Err(AppError::new(
400,
"MPUT requires a Map of key -> value in the body",
)),
}
}
fn text_of(value: &Value) -> String {
match value {
Value::String(text) => text.as_str().unwrap_or_default().to_string(),
other => other.to_string(),
}
}