phantom_frame/
lib.rs

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/// Information about an incoming request for cache key generation
13#[derive(Clone, Debug)]
14pub struct RequestInfo<'a> {
15    /// HTTP method (e.g., "GET", "POST", "PUT", "DELETE")
16    pub method: &'a str,
17    /// Request path (e.g., "/api/users")
18    pub path: &'a str,
19    /// Query string (e.g., "id=123&sort=asc")
20    pub query: &'a str,
21    /// Request headers (for custom cache key logic based on headers)
22    pub headers: &'a axum::http::HeaderMap,
23}
24
25/// Configuration for creating a proxy
26#[derive(Clone)]
27pub struct CreateProxyConfig {
28    /// The backend URL to proxy requests to
29    pub proxy_url: String,
30    
31    /// Paths to include in caching (empty means include all)
32    /// Supports wildcards and method prefixes: "/api/*", "POST /api/*", "GET /*/users", etc.
33    pub include_paths: Vec<String>,
34    
35    /// Paths to exclude from caching (empty means exclude none)
36    /// Supports wildcards and method prefixes: "/admin/*", "POST *", "PUT /api/*", etc.
37    /// Exclude overrides include
38    pub exclude_paths: Vec<String>,
39    
40    /// Custom cache key generator
41    /// Takes request info and returns a cache key
42    /// Default: method + path + query string
43    pub cache_key_fn: Arc<dyn Fn(&RequestInfo) -> String + Send + Sync>,
44}
45
46impl CreateProxyConfig {
47    /// Create a new config with default settings
48    pub fn new(proxy_url: String) -> Self {
49        Self {
50            proxy_url,
51            include_paths: vec![],
52            exclude_paths: vec![],
53            cache_key_fn: Arc::new(|req_info| {
54                if req_info.query.is_empty() {
55                    format!("{}:{}", req_info.method, req_info.path)
56                } else {
57                    format!("{}:{}?{}", req_info.method, req_info.path, req_info.query)
58                }
59            }),
60        }
61    }
62    
63    /// Set include paths
64    pub fn with_include_paths(mut self, paths: Vec<String>) -> Self {
65        self.include_paths = paths;
66        self
67    }
68    
69    /// Set exclude paths
70    pub fn with_exclude_paths(mut self, paths: Vec<String>) -> Self {
71        self.exclude_paths = paths;
72        self
73    }
74    
75    /// Set custom cache key function
76    pub fn with_cache_key_fn<F>(mut self, f: F) -> Self
77    where
78        F: Fn(&RequestInfo) -> String + Send + Sync + 'static,
79    {
80        self.cache_key_fn = Arc::new(f);
81        self
82    }
83}
84
85/// The main library interface for using phantom-frame as a library
86/// Returns a proxy handler function and a refresh trigger
87pub fn create_proxy(config: CreateProxyConfig) -> (Router, RefreshTrigger) {
88    let refresh_trigger = RefreshTrigger::new();
89    let cache = CacheStore::new(refresh_trigger.clone());
90
91    // Spawn background task to listen for refresh events
92    spawn_refresh_listener(cache.clone());
93
94    let proxy_state = Arc::new(ProxyState::new(cache, config));
95
96    let app = Router::new()
97        .fallback(proxy::proxy_handler)
98        .layer(Extension(proxy_state));
99
100    (app, refresh_trigger)
101}
102
103/// Create a proxy handler with an existing refresh trigger
104pub fn create_proxy_with_trigger(config: CreateProxyConfig, refresh_trigger: RefreshTrigger) -> Router {
105    let cache = CacheStore::new(refresh_trigger);
106    
107    // Spawn background task to listen for refresh events
108    spawn_refresh_listener(cache.clone());
109
110    let proxy_state = Arc::new(ProxyState::new(cache, config));
111
112    Router::new()
113        .fallback(proxy::proxy_handler)
114        .layer(Extension(proxy_state))
115}
116
117/// Spawn a background task to listen for refresh events
118fn spawn_refresh_listener(cache: CacheStore) {
119    let mut receiver = cache.refresh_trigger().subscribe();
120    
121    tokio::spawn(async move {
122        loop {
123            match receiver.recv().await {
124                Ok(cache::RefreshMessage::All) => {
125                    tracing::info!("Cache refresh triggered: clearing all entries");
126                    cache.clear().await;
127                }
128                Ok(cache::RefreshMessage::Pattern(pattern)) => {
129                    tracing::info!("Cache refresh triggered: clearing entries matching pattern '{}'", pattern);
130                    cache.clear_by_pattern(&pattern).await;
131                }
132                Err(e) => {
133                    tracing::error!("Refresh trigger channel error: {}", e);
134                    break;
135                }
136            }
137        }
138    });
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[tokio::test]
146    async fn test_create_proxy() {
147        let config = CreateProxyConfig::new("http://localhost:8080".to_string());
148        let (_app, trigger) = create_proxy(config);
149        trigger.trigger();
150        trigger.trigger_by_key_match("GET:/api/*");
151        // Just ensure it compiles and runs without panic
152    }
153}