1use std::time::Duration;
2
3use zeroize::Zeroizing;
4
5use crate::error::CachekitError;
6
7pub struct CachekitConfig {
11 pub api_key: Option<Zeroizing<String>>,
13 pub api_url: String,
15 pub master_key: Option<Zeroizing<Vec<u8>>>,
17 pub default_ttl: Duration,
19 pub namespace: Option<String>,
21 pub l1_capacity: usize,
23 pub max_payload_bytes: usize,
25}
26
27impl std::fmt::Debug for CachekitConfig {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 let api_key_repr = if self.api_key.is_some() {
30 "[REDACTED]"
31 } else {
32 "None"
33 };
34 let master_key_repr = if self.master_key.is_some() {
35 "[REDACTED]"
36 } else {
37 "None"
38 };
39
40 f.debug_struct("CachekitConfig")
41 .field("api_key", &api_key_repr)
42 .field("api_url", &self.api_url)
43 .field("master_key", &master_key_repr)
44 .field("default_ttl", &self.default_ttl)
45 .field("namespace", &self.namespace)
46 .field("l1_capacity", &self.l1_capacity)
47 .field("max_payload_bytes", &self.max_payload_bytes)
48 .finish()
49 }
50}
51
52impl Default for CachekitConfig {
53 fn default() -> Self {
54 Self {
55 api_key: None,
56 api_url: "https://api.cachekit.io".to_owned(),
57 master_key: None,
58 default_ttl: Duration::from_secs(300),
59 namespace: None,
60 l1_capacity: 1000,
61 max_payload_bytes: 5 * 1024 * 1024, }
63 }
64}
65
66impl CachekitConfig {
67 pub fn from_env() -> Result<Self, CachekitError> {
76 let mut config = Self::default();
77
78 if let Ok(val) = std::env::var("CACHEKIT_API_KEY") {
80 config.api_key = Some(Zeroizing::new(val));
81 }
82
83 if let Ok(val) = std::env::var("CACHEKIT_API_URL") {
85 validate_https(&val)?;
86 config.api_url = val;
87 }
88
89 if let Ok(val) = std::env::var("CACHEKIT_MASTER_KEY") {
91 let bytes = hex::decode(&val).map_err(|e| {
92 CachekitError::Config(format!("CACHEKIT_MASTER_KEY is not valid hex: {e}"))
93 })?;
94 if bytes.len() < 32 {
95 return Err(CachekitError::Config(format!(
96 "CACHEKIT_MASTER_KEY must be at least 32 bytes ({} hex chars); got {} bytes",
97 64,
98 bytes.len()
99 )));
100 }
101 config.master_key = Some(Zeroizing::new(bytes));
102 }
103
104 if let Ok(val) = std::env::var("CACHEKIT_DEFAULT_TTL") {
106 let secs: u64 = val.parse().map_err(|e| {
107 CachekitError::Config(format!("CACHEKIT_DEFAULT_TTL must be an integer: {e}"))
108 })?;
109 if secs < 1 {
110 return Err(CachekitError::Config(
111 "CACHEKIT_DEFAULT_TTL must be at least 1 second".to_owned(),
112 ));
113 }
114 config.default_ttl = Duration::from_secs(secs);
115 }
116
117 Ok(config)
118 }
119}
120
121#[derive(Default)]
125#[must_use]
126pub struct CachekitConfigBuilder {
127 inner: CachekitConfig,
128}
129
130impl CachekitConfigBuilder {
131 pub fn new() -> Self {
133 Self {
134 inner: CachekitConfig::default(),
135 }
136 }
137
138 pub fn api_key(mut self, key: impl Into<String>) -> Self {
140 self.inner.api_key = Some(Zeroizing::new(key.into()));
141 self
142 }
143
144 pub fn api_url(mut self, url: impl Into<String>) -> Result<Self, CachekitError> {
146 let url = url.into();
147 validate_https(&url)?;
148 self.inner.api_url = url;
149 Ok(self)
150 }
151
152 pub fn master_key(mut self, hex_key: &str) -> Result<Self, CachekitError> {
154 let bytes = hex::decode(hex_key)
155 .map_err(|e| CachekitError::Config(format!("master_key is not valid hex: {e}")))?;
156 if bytes.len() < 32 {
157 return Err(CachekitError::Config(format!(
158 "master_key must be at least 32 bytes; got {}",
159 bytes.len()
160 )));
161 }
162 self.inner.master_key = Some(Zeroizing::new(bytes));
163 Ok(self)
164 }
165
166 pub fn default_ttl(mut self, ttl: Duration) -> Result<Self, CachekitError> {
168 if ttl < Duration::from_secs(1) {
169 return Err(CachekitError::Config(
170 "default_ttl must be at least 1 second".to_owned(),
171 ));
172 }
173 self.inner.default_ttl = ttl;
174 Ok(self)
175 }
176
177 pub fn namespace(mut self, ns: impl Into<String>) -> Self {
179 self.inner.namespace = Some(ns.into());
180 self
181 }
182
183 pub fn l1_capacity(mut self, capacity: usize) -> Self {
185 self.inner.l1_capacity = capacity;
186 self
187 }
188
189 pub fn build(self) -> CachekitConfig {
191 self.inner
192 }
193}
194
195fn validate_https(url: &str) -> Result<(), CachekitError> {
198 if !url.starts_with("https://") {
199 return Err(CachekitError::Config(format!(
200 "API URL must use HTTPS; got: {url}"
201 )));
202 }
203 Ok(())
204}