Skip to main content

cache_manager/interceptors/
cache_interceptor.rs

1//! Port map for upstream `lib/interceptors/cache.interceptor.ts`.
2
3use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use serde::Serialize;
7use serde_json::Value;
8
9use crate::cache_constants::{CACHE_KEY_METADATA, CACHE_TTL_METADATA};
10use crate::cache_providers::{CacheManager, CacheManagerError, CacheValue};
11use crate::decorators::{CacheKeyMetadata, CacheTTLMetadata};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Request {
15    pub method: String,
16    pub url: String,
17}
18
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
20pub struct Response {
21    pub headers: BTreeMap<String, String>,
22}
23
24#[derive(Debug, Clone, Default, PartialEq, Eq)]
25pub struct HttpAdapter;
26
27impl HttpAdapter {
28    pub fn getRequestMethod(&self, request: &Request) -> String {
29        request.method.clone()
30    }
31
32    pub fn getRequestUrl(&self, request: &Request) -> String {
33        request.url.clone()
34    }
35
36    pub fn setHeader(&self, response: &mut Response, name: &str, value: &str) {
37        response.headers.insert(name.to_string(), value.to_string());
38    }
39}
40
41#[derive(Clone, Default)]
42pub struct ExecutionContext {
43    pub handler: String,
44    pub class_name: String,
45    pub request: Option<Request>,
46    pub response: Option<Response>,
47    pub http_adapter: Option<HttpAdapter>,
48    cache_key_metadata: Option<CacheKeyMetadata>,
49    cache_ttl_metadata: Option<CacheTTLMetadata>,
50    class_cache_ttl_metadata: Option<CacheTTLMetadata>,
51}
52
53impl ExecutionContext {
54    pub fn http(
55        handler: impl Into<String>,
56        class_name: impl Into<String>,
57        request: Request,
58    ) -> Self {
59        Self {
60            handler: handler.into(),
61            class_name: class_name.into(),
62            request: Some(request),
63            response: Some(Response::default()),
64            http_adapter: Some(HttpAdapter),
65            ..Self::default()
66        }
67    }
68
69    pub fn set_metadata(&mut self, key: &str, value: Metadata) {
70        match (key, value) {
71            (CACHE_KEY_METADATA, Metadata::CacheKey(value)) => {
72                self.cache_key_metadata = Some(value)
73            }
74            (CACHE_TTL_METADATA, Metadata::CacheTTL(value)) => {
75                self.cache_ttl_metadata = Some(value)
76            }
77            _ => {}
78        }
79    }
80
81    pub fn set_class_cache_ttl(&mut self, value: CacheTTLMetadata) {
82        self.class_cache_ttl_metadata = Some(value);
83    }
84
85    pub fn response(&self) -> Option<&Response> {
86        self.response.as_ref()
87    }
88}
89
90#[derive(Clone)]
91pub enum Metadata {
92    CacheKey(CacheKeyMetadata),
93    CacheTTL(CacheTTLMetadata),
94}
95
96#[derive(Debug, Clone, PartialEq)]
97pub enum InterceptorResponse {
98    Cached(CacheValue),
99    Handled(CacheValue),
100    StreamableFile,
101}
102
103pub type CallHandler =
104    Arc<dyn Fn() -> Result<InterceptorResponse, CacheManagerError> + Send + Sync + 'static>;
105
106#[derive(Clone)]
107pub struct CacheInterceptor {
108    pub cacheManager: CacheManager,
109    pub allowedMethods: Vec<String>,
110}
111
112impl CacheInterceptor {
113    pub fn new(cacheManager: CacheManager) -> Self {
114        Self {
115            cacheManager,
116            allowedMethods: vec!["GET".to_string()],
117        }
118    }
119
120    pub async fn intercept(
121        &self,
122        context: &mut ExecutionContext,
123        next: CallHandler,
124    ) -> Result<InterceptorResponse, CacheManagerError> {
125        let key = self.trackBy(context);
126        let ttlValueOrFactory = context
127            .cache_ttl_metadata
128            .clone()
129            .or_else(|| context.class_cache_ttl_metadata.clone());
130
131        let Some(key) = key else {
132            return next();
133        };
134
135        match self.cacheManager.get(&key).await {
136            Ok(value) => {
137                self.setHeadersWhenHttp(context, value.as_ref());
138
139                if let Some(value) = value {
140                    return Ok(InterceptorResponse::Cached(value));
141                }
142
143                let response = next()?;
144                if !matches!(response, InterceptorResponse::StreamableFile) {
145                    let ttl = ttlValueOrFactory.map(|ttl| ttl.resolve(context));
146                    if let Some(value) = response_value(&response) {
147                        if let Err(err) = self
148                            .cacheManager
149                            .set_value(
150                                &key,
151                                value.clone(),
152                                ttl.map(std::time::Duration::from_millis),
153                            )
154                            .await
155                        {
156                            eprintln!(
157                                "An error has occurred when inserting \"key: {key}\", \"value: {value}\""
158                            );
159                            eprintln!("{err}");
160                        }
161                    }
162                }
163                Ok(response)
164            }
165            Err(_) => next(),
166        }
167    }
168
169    pub fn trackBy(&self, context: &ExecutionContext) -> Option<String> {
170        let isHttpApp = context.http_adapter.is_some();
171        let cacheMetadataOrFactory = context.cache_key_metadata.clone();
172
173        if !isHttpApp || cacheMetadataOrFactory.is_some() {
174            return cacheMetadataOrFactory.and_then(|metadata| metadata.resolve(context));
175        }
176
177        if !self.isRequestCacheable(context) {
178            return None;
179        }
180
181        let request = context.request.as_ref()?;
182        let http_adapter = context.http_adapter.as_ref()?;
183        Some(http_adapter.getRequestUrl(request))
184    }
185
186    pub fn isRequestCacheable(&self, context: &ExecutionContext) -> bool {
187        context
188            .request
189            .as_ref()
190            .map(|req| self.allowedMethods.contains(&req.method))
191            .unwrap_or(false)
192    }
193
194    pub fn setHeadersWhenHttp(&self, context: &mut ExecutionContext, value: Option<&CacheValue>) {
195        let Some(http_adapter) = context.http_adapter.as_ref() else {
196            return;
197        };
198        let Some(response) = context.response.as_mut() else {
199            return;
200        };
201        http_adapter.setHeader(
202            response,
203            "X-Cache",
204            if value.is_some() { "HIT" } else { "MISS" },
205        );
206    }
207}
208
209fn response_value(response: &InterceptorResponse) -> Option<&Value> {
210    match response {
211        InterceptorResponse::Cached(value) | InterceptorResponse::Handled(value) => Some(value),
212        InterceptorResponse::StreamableFile => None,
213    }
214}
215
216pub fn handled<T: Serialize>(value: T) -> Result<InterceptorResponse, CacheManagerError> {
217    Ok(InterceptorResponse::Handled(serde_json::to_value(value)?))
218}