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}
55
56impl CreateProxyConfig {
57 pub fn new(proxy_url: String) -> Self {
59 Self {
60 proxy_url,
61 include_paths: vec![],
62 exclude_paths: vec![],
63 enable_websocket: true,
64 forward_get_only: false,
65 cache_key_fn: Arc::new(|req_info| {
66 if req_info.query.is_empty() {
67 format!("{}:{}", req_info.method, req_info.path)
68 } else {
69 format!("{}:{}?{}", req_info.method, req_info.path, req_info.query)
70 }
71 }),
72 }
73 }
74
75 pub fn with_include_paths(mut self, paths: Vec<String>) -> Self {
77 self.include_paths = paths;
78 self
79 }
80
81 pub fn with_exclude_paths(mut self, paths: Vec<String>) -> Self {
83 self.exclude_paths = paths;
84 self
85 }
86
87 pub fn with_websocket_enabled(mut self, enabled: bool) -> Self {
89 self.enable_websocket = enabled;
90 self
91 }
92
93 pub fn with_forward_get_only(mut self, enabled: bool) -> Self {
95 self.forward_get_only = enabled;
96 self
97 }
98
99 pub fn with_cache_key_fn<F>(mut self, f: F) -> Self
101 where
102 F: Fn(&RequestInfo) -> String + Send + Sync + 'static,
103 {
104 self.cache_key_fn = Arc::new(f);
105 self
106 }
107}
108
109pub fn create_proxy(config: CreateProxyConfig) -> (Router, RefreshTrigger) {
112 let refresh_trigger = RefreshTrigger::new();
113 let cache = CacheStore::new(refresh_trigger.clone());
114
115 spawn_refresh_listener(cache.clone());
117
118 let proxy_state = Arc::new(ProxyState::new(cache, config));
119
120 let app = Router::new()
121 .fallback(proxy::proxy_handler)
122 .layer(Extension(proxy_state));
123
124 (app, refresh_trigger)
125}
126
127pub fn create_proxy_with_trigger(config: CreateProxyConfig, refresh_trigger: RefreshTrigger) -> Router {
129 let cache = CacheStore::new(refresh_trigger);
130
131 spawn_refresh_listener(cache.clone());
133
134 let proxy_state = Arc::new(ProxyState::new(cache, config));
135
136 Router::new()
137 .fallback(proxy::proxy_handler)
138 .layer(Extension(proxy_state))
139}
140
141fn spawn_refresh_listener(cache: CacheStore) {
143 let mut receiver = cache.refresh_trigger().subscribe();
144
145 tokio::spawn(async move {
146 loop {
147 match receiver.recv().await {
148 Ok(cache::RefreshMessage::All) => {
149 tracing::debug!("Cache refresh triggered: clearing all entries");
150 cache.clear().await;
151 }
152 Ok(cache::RefreshMessage::Pattern(pattern)) => {
153 tracing::debug!("Cache refresh triggered: clearing entries matching pattern '{}'", pattern);
154 cache.clear_by_pattern(&pattern).await;
155 }
156 Err(e) => {
157 tracing::error!("Refresh trigger channel error: {}", e);
158 break;
159 }
160 }
161 }
162 });
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[tokio::test]
170 async fn test_create_proxy() {
171 let config = CreateProxyConfig::new("http://localhost:8080".to_string());
172 let (_app, trigger) = create_proxy(config);
173 trigger.trigger();
174 trigger.trigger_by_key_match("GET:/api/*");
175 }
177}