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