1pub mod cache;
2pub mod config;
3pub mod control;
4pub mod path_matcher;
5pub mod proxy;
6
7use axum::{extract::Extension, Router};
8use cache::{CacheStore, RefreshTrigger};
9use proxy::ProxyState;
10use std::sync::Arc;
11
12#[derive(Clone, Debug)]
14pub struct RequestInfo<'a> {
15 pub method: &'a str,
17 pub path: &'a str,
19 pub query: &'a str,
21 pub headers: &'a axum::http::HeaderMap,
23}
24
25#[derive(Clone)]
27pub struct CreateProxyConfig {
28 pub proxy_url: String,
30
31 pub include_paths: Vec<String>,
34
35 pub exclude_paths: Vec<String>,
39
40 pub enable_websocket: bool,
44
45 pub forward_get_only: bool,
49
50 pub cache_key_fn: Arc<dyn Fn(&RequestInfo) -> String + Send + Sync>,
54 pub cache_404_capacity: usize,
56
57 pub use_404_meta: bool,
60}
61
62impl CreateProxyConfig {
63 pub fn new(proxy_url: String) -> Self {
65 Self {
66 proxy_url,
67 include_paths: vec![],
68 exclude_paths: vec![],
69 enable_websocket: true,
70 forward_get_only: false,
71 cache_key_fn: Arc::new(|req_info| {
72 if req_info.query.is_empty() {
73 format!("{}:{}", req_info.method, req_info.path)
74 } else {
75 format!("{}:{}?{}", req_info.method, req_info.path, req_info.query)
76 }
77 }),
78 cache_404_capacity: 100,
79 use_404_meta: false,
80 }
81 }
82
83 pub fn with_include_paths(mut self, paths: Vec<String>) -> Self {
85 self.include_paths = paths;
86 self
87 }
88
89 pub fn with_exclude_paths(mut self, paths: Vec<String>) -> Self {
91 self.exclude_paths = paths;
92 self
93 }
94
95 pub fn with_websocket_enabled(mut self, enabled: bool) -> Self {
97 self.enable_websocket = enabled;
98 self
99 }
100
101 pub fn with_forward_get_only(mut self, enabled: bool) -> Self {
103 self.forward_get_only = enabled;
104 self
105 }
106
107 pub fn with_cache_key_fn<F>(mut self, f: F) -> Self
109 where
110 F: Fn(&RequestInfo) -> String + Send + Sync + 'static,
111 {
112 self.cache_key_fn = Arc::new(f);
113 self
114 }
115
116 pub fn with_cache_404_capacity(mut self, capacity: usize) -> Self {
118 self.cache_404_capacity = capacity;
119 self
120 }
121
122 pub fn with_use_404_meta(mut self, enabled: bool) -> Self {
124 self.use_404_meta = enabled;
125 self
126 }
127}
128
129pub fn create_proxy(config: CreateProxyConfig) -> (Router, RefreshTrigger) {
132 let refresh_trigger = RefreshTrigger::new();
133 let cache = CacheStore::new(refresh_trigger.clone(), config.cache_404_capacity);
134
135 spawn_refresh_listener(cache.clone());
137
138 let proxy_state = Arc::new(ProxyState::new(cache, config));
139
140 let app = Router::new()
141 .fallback(proxy::proxy_handler)
142 .layer(Extension(proxy_state));
143
144 (app, refresh_trigger)
145}
146
147pub fn create_proxy_with_trigger(config: CreateProxyConfig, refresh_trigger: RefreshTrigger) -> Router {
149 let cache = CacheStore::new(refresh_trigger, 100);
150
151 spawn_refresh_listener(cache.clone());
153
154 let proxy_state = Arc::new(ProxyState::new(cache, config));
155
156 Router::new()
157 .fallback(proxy::proxy_handler)
158 .layer(Extension(proxy_state))
159}
160
161fn spawn_refresh_listener(cache: CacheStore) {
163 let mut receiver = cache.refresh_trigger().subscribe();
164
165 tokio::spawn(async move {
166 loop {
167 match receiver.recv().await {
168 Ok(cache::RefreshMessage::All) => {
169 tracing::debug!("Cache refresh triggered: clearing all entries");
170 cache.clear().await;
171 }
172 Ok(cache::RefreshMessage::Pattern(pattern)) => {
173 tracing::debug!("Cache refresh triggered: clearing entries matching pattern '{}'", pattern);
174 cache.clear_by_pattern(&pattern).await;
175 }
176 Err(e) => {
177 tracing::error!("Refresh trigger channel error: {}", e);
178 break;
179 }
180 }
181 }
182 });
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[tokio::test]
190 async fn test_create_proxy() {
191 let config = CreateProxyConfig::new("http://localhost:8080".to_string());
192 let (_app, trigger) = create_proxy(config);
193 trigger.trigger();
194 trigger.trigger_by_key_match("GET:/api/*");
195 }
197}