use std::time::Duration;
use zeroize::Zeroizing;
use crate::error::CachekitError;
pub struct CachekitConfig {
pub api_key: Option<Zeroizing<String>>,
pub api_url: String,
pub master_key: Option<Zeroizing<Vec<u8>>>,
pub default_ttl: Duration,
pub namespace: Option<String>,
pub l1_capacity: usize,
pub max_payload_bytes: usize,
}
impl std::fmt::Debug for CachekitConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let api_key_repr = if self.api_key.is_some() {
"[REDACTED]"
} else {
"None"
};
let master_key_repr = if self.master_key.is_some() {
"[REDACTED]"
} else {
"None"
};
f.debug_struct("CachekitConfig")
.field("api_key", &api_key_repr)
.field("api_url", &self.api_url)
.field("master_key", &master_key_repr)
.field("default_ttl", &self.default_ttl)
.field("namespace", &self.namespace)
.field("l1_capacity", &self.l1_capacity)
.field("max_payload_bytes", &self.max_payload_bytes)
.finish()
}
}
impl Default for CachekitConfig {
fn default() -> Self {
Self {
api_key: None,
api_url: "https://api.cachekit.io".to_owned(),
master_key: None,
default_ttl: Duration::from_secs(300),
namespace: None,
l1_capacity: 1000,
max_payload_bytes: 5 * 1024 * 1024, }
}
}
impl CachekitConfig {
pub fn from_env() -> Result<Self, CachekitError> {
let mut config = Self::default();
if let Ok(val) = std::env::var("CACHEKIT_API_KEY") {
config.api_key = Some(Zeroizing::new(val));
}
if let Ok(val) = std::env::var("CACHEKIT_API_URL") {
validate_https(&val)?;
config.api_url = val;
}
if let Ok(val) = std::env::var("CACHEKIT_MASTER_KEY") {
let bytes = hex::decode(&val).map_err(|e| {
CachekitError::Config(format!("CACHEKIT_MASTER_KEY is not valid hex: {e}"))
})?;
if bytes.len() < 32 {
return Err(CachekitError::Config(format!(
"CACHEKIT_MASTER_KEY must be at least 32 bytes ({} hex chars); got {} bytes",
64,
bytes.len()
)));
}
config.master_key = Some(Zeroizing::new(bytes));
}
if let Ok(val) = std::env::var("CACHEKIT_DEFAULT_TTL") {
let secs: u64 = val.parse().map_err(|e| {
CachekitError::Config(format!("CACHEKIT_DEFAULT_TTL must be an integer: {e}"))
})?;
if secs < 1 {
return Err(CachekitError::Config(
"CACHEKIT_DEFAULT_TTL must be at least 1 second".to_owned(),
));
}
config.default_ttl = Duration::from_secs(secs);
}
Ok(config)
}
}
#[derive(Default)]
#[must_use]
pub struct CachekitConfigBuilder {
inner: CachekitConfig,
}
impl CachekitConfigBuilder {
pub fn new() -> Self {
Self {
inner: CachekitConfig::default(),
}
}
pub fn api_key(mut self, key: impl Into<String>) -> Self {
self.inner.api_key = Some(Zeroizing::new(key.into()));
self
}
pub fn api_url(mut self, url: impl Into<String>) -> Result<Self, CachekitError> {
let url = url.into();
validate_https(&url)?;
self.inner.api_url = url;
Ok(self)
}
pub fn master_key(mut self, hex_key: &str) -> Result<Self, CachekitError> {
let bytes = hex::decode(hex_key)
.map_err(|e| CachekitError::Config(format!("master_key is not valid hex: {e}")))?;
if bytes.len() < 32 {
return Err(CachekitError::Config(format!(
"master_key must be at least 32 bytes; got {}",
bytes.len()
)));
}
self.inner.master_key = Some(Zeroizing::new(bytes));
Ok(self)
}
pub fn default_ttl(mut self, ttl: Duration) -> Result<Self, CachekitError> {
if ttl < Duration::from_secs(1) {
return Err(CachekitError::Config(
"default_ttl must be at least 1 second".to_owned(),
));
}
self.inner.default_ttl = ttl;
Ok(self)
}
pub fn namespace(mut self, ns: impl Into<String>) -> Self {
self.inner.namespace = Some(ns.into());
self
}
pub fn l1_capacity(mut self, capacity: usize) -> Self {
self.inner.l1_capacity = capacity;
self
}
pub fn build(self) -> CachekitConfig {
self.inner
}
}
fn validate_https(url: &str) -> Result<(), CachekitError> {
if !url.starts_with("https://") {
return Err(CachekitError::Config(format!(
"API URL must use HTTPS; got: {url}"
)));
}
Ok(())
}