cachekit/backend/
cachekitio_ttl.rs1use std::time::Duration;
4
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7
8use super::cachekitio::{reqwest_err_sanitized, CachekitIO};
9use super::TtlInspectable;
10use crate::error::BackendError;
11
12#[derive(Deserialize)]
13#[serde(deny_unknown_fields)]
14struct TtlResponse {
15 ttl: Option<u64>,
16}
17
18#[derive(Serialize)]
19struct RefreshTtlRequest {
20 ttl: u64,
21}
22
23#[cfg(not(target_arch = "wasm32"))]
24#[cfg_attr(not(feature = "unsync"), async_trait)]
25#[cfg_attr(feature = "unsync", async_trait(?Send))]
26impl TtlInspectable for CachekitIO {
27 async fn ttl(&self, key: &str) -> Result<Option<Duration>, BackendError> {
28 let url = self.ttl_url(key)?;
29
30 let req =
31 self.with_standard_headers(self.client().get(&url).bearer_auth(self.api_key_str()));
32
33 let resp = req
34 .send()
35 .await
36 .map_err(|e| reqwest_err_sanitized(e, self.api_key_str()))?;
37
38 match resp.status().as_u16() {
39 200 => {
40 let body: TtlResponse = resp.json().await.map_err(|e| {
41 BackendError::transient(format!("failed to parse TTL response: {e}"))
42 })?;
43 Ok(body.ttl.map(Duration::from_secs))
44 }
45 404 => Ok(None),
46 _ => Err(self.error_from_response(resp).await),
47 }
48 }
49
50 async fn refresh_ttl(&self, key: &str, ttl: Duration) -> Result<bool, BackendError> {
51 let secs = ttl.as_secs();
52 if secs == 0 {
53 return Err(BackendError::permanent(
54 "refresh_ttl requires at least 1 second".to_string(),
55 ));
56 }
57
58 let url = self.ttl_url(key)?;
59
60 let body = serde_json::to_vec(&RefreshTtlRequest { ttl: secs }).map_err(|e| {
61 BackendError::permanent(format!("failed to serialize refresh_ttl request: {e}"))
62 })?;
63
64 let req = self.with_standard_headers(
65 self.client()
66 .patch(&url)
67 .bearer_auth(self.api_key_str())
68 .header("Content-Type", "application/json")
69 .body(body),
70 );
71
72 let resp = req
73 .send()
74 .await
75 .map_err(|e| reqwest_err_sanitized(e, self.api_key_str()))?;
76
77 match resp.status().as_u16() {
78 200 | 204 => Ok(true),
79 404 => Ok(false),
80 _ => Err(self.error_from_response(resp).await),
81 }
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 fn _assert_ttl_inspectable(_b: &dyn TtlInspectable) {}
91
92 #[test]
93 fn cachekitio_is_ttl_inspectable() {
94 fn _check(backend: &CachekitIO) {
95 _assert_ttl_inspectable(backend);
96 }
97 }
98
99 #[test]
102 fn ttl_response_decodes_null_ttl_as_none() {
103 let decode = |json: &str| serde_json::from_str::<TtlResponse>(json).map(|r| r.ttl);
104
105 assert!(matches!(decode(r#"{"ttl":null}"#), Ok(None)));
106 assert!(matches!(decode(r#"{"ttl":42}"#), Ok(Some(42))));
107 assert!(decode(r#"{"ttl":null,"extra":1}"#).is_err());
108 }
109}