rustisan_core/
app.rs

1//! Application module for the Rustisan framework
2//!
3//! This module provides the main Application struct that coordinates
4//! the entire web application, similar to Laravel's Application.
5
6use std::collections::HashMap;
7use std::net::SocketAddr;
8use std::sync::Arc;
9
10use axum::{
11    extract::{Path, Query},
12    http::Method,
13    middleware::{self, Next},
14    response::{IntoResponse, Response as AxumResponse},
15    routing::{delete, get, patch, post, put},
16    Json, Router as AxumRouter,
17};
18use serde_json::{json, Value};
19use tokio::net::TcpListener;
20use tower::ServiceBuilder;
21use tower_http::{
22    cors::CorsLayer,
23    trace::TraceLayer,
24};
25use tracing::{info, warn};
26
27use crate::{
28    config::Config,
29    errors::{Result, RustisanError},
30    routing::{Route, RouteGroup, Router},
31};
32
33/// The main application struct that manages the entire web application
34pub struct Application {
35    /// Application configuration
36    config: Config,
37    /// Application router
38    router: Router,
39    /// Application state
40    state: HashMap<String, Value>,
41    /// Whether the application has been built
42    built: bool,
43    /// The built Axum router
44    axum_router: Option<AxumRouter>,
45}
46
47/// Application state that can be shared across handlers
48#[derive(Clone)]
49pub struct AppState {
50    /// Application configuration
51    pub config: Arc<Config>,
52    /// Shared application data
53    pub data: Arc<HashMap<String, Value>>,
54}
55
56impl Application {
57    /// Creates a new Rustisan application
58    pub fn new() -> Self {
59        Self {
60            config: Config::default(),
61            router: Router::new(),
62            state: HashMap::new(),
63            built: false,
64            axum_router: None,
65        }
66    }
67
68    /// Creates a new application with custom configuration
69    pub fn with_config(config: Config) -> Self {
70        Self {
71            config,
72            router: Router::new(),
73            state: HashMap::new(),
74            built: false,
75            axum_router: None,
76        }
77    }
78
79    /// Gets a mutable reference to the router for route registration
80    pub fn router(&mut self) -> &mut Router {
81        &mut self.router
82    }
83
84    /// Sets application state
85    pub fn set_state<K, V>(&mut self, key: K, value: V)
86    where
87        K: Into<String>,
88        V: Into<Value>,
89    {
90        self.state.insert(key.into(), value.into());
91    }
92
93    /// Gets application state
94    pub fn get_state(&self, key: &str) -> Option<&Value> {
95        self.state.get(key)
96    }
97
98    /// Builds the application and prepares it for serving
99    pub async fn build(&mut self) -> Result<()> {
100        if self.built {
101            return Ok(());
102        }
103
104        info!("🔧 Building Rustisan application...");
105
106        // Create the app state
107        let app_state = AppState {
108            config: Arc::new(self.config.clone()),
109            data: Arc::new(self.state.clone()),
110        };
111
112        // Start with a new Axum router
113        let mut axum_router = AxumRouter::new();
114
115        // Register all routes
116        axum_router = self.register_routes(axum_router).await?;
117
118        // Add middleware
119        axum_router = axum_router
120            .layer(
121                ServiceBuilder::new()
122                    .layer(TraceLayer::new_for_http())
123                    .layer(CorsLayer::permissive())
124                    .layer(middleware::from_fn(error_handler)),
125            );
126
127        self.axum_router = Some(axum_router);
128        self.built = true;
129
130        info!("✅ Application built successfully");
131        Ok(())
132    }
133
134    /// Registers all routes with the Axum router
135    async fn register_routes(
136        &self,
137        mut axum_router: AxumRouter,
138    ) -> Result<AxumRouter> {
139        info!("📍 Registering routes...");
140
141        // Register individual routes
142        for route in &self.router.routes {
143            axum_router = self.register_single_route(axum_router, route)?;
144        }
145
146        // Register route groups
147        for group in &self.router.groups {
148            axum_router = self.register_group_routes(axum_router, group)?;
149        }
150
151        info!("📍 {} routes registered", self.router.routes.len());
152        Ok(axum_router)
153    }
154
155    /// Registers a single route
156    fn register_single_route(
157        &self,
158        axum_router: AxumRouter,
159        route: &Route,
160    ) -> Result<AxumRouter> {
161        let path = route.path.clone();
162        let method = route.method.clone();
163
164        // Create a simple handler that returns a JSON response
165        let handler = move || async move {
166            Json(json!({
167                "message": format!("Route {} {} handled", method, path),
168                "status": "success"
169            }))
170        };
171
172        let router = match route.method {
173            Method::GET => axum_router.route(&route.path, get(handler)),
174            Method::POST => axum_router.route(&route.path, post(handler)),
175            Method::PUT => axum_router.route(&route.path, put(handler)),
176            Method::PATCH => axum_router.route(&route.path, patch(handler)),
177            Method::DELETE => axum_router.route(&route.path, delete(handler)),
178            _ => {
179                warn!("Unsupported HTTP method: {}", route.method);
180                return Ok(axum_router);
181            }
182        };
183
184        Ok(router)
185    }
186
187    /// Registers routes from a group
188    fn register_group_routes(
189        &self,
190        mut axum_router: AxumRouter,
191        group: &RouteGroup,
192    ) -> Result<AxumRouter> {
193        for route in &group.routes {
194            axum_router = self.register_single_route(axum_router, route)?;
195        }
196        Ok(axum_router)
197    }
198
199    /// Starts the HTTP server
200    pub async fn serve(&mut self, addr: SocketAddr) -> Result<()> {
201        if !self.built {
202            self.build().await?;
203        }
204
205        let axum_router = self
206            .axum_router
207            .take()
208            .ok_or_else(|| RustisanError::InternalError("Application not built".to_string()))?;
209
210        info!("🚀 Starting Rustisan server on {}", addr);
211        info!("📚 Routes available:");
212
213        // Log all registered routes
214        for route in &self.router.routes {
215            info!("   {} {}", route.method, route.path);
216        }
217
218        for group in &self.router.groups {
219            for route in &group.routes {
220                info!("   {} {}", route.method, route.path);
221            }
222        }
223
224        let listener = TcpListener::bind(addr).await.map_err(|e| {
225            RustisanError::InternalError(format!("Failed to bind to address {}: {}", addr, e))
226        })?;
227
228        info!("✅ Server running on http://{}", addr);
229
230        axum::serve(listener, axum_router).await.map_err(|e| {
231            RustisanError::InternalError(format!("Server error: {}", e))
232        })?;
233
234        Ok(())
235    }
236
237    /// Convenience method to serve on default address
238    pub async fn serve_default(&mut self) -> Result<()> {
239        let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
240        self.serve(addr).await
241    }
242
243    /// Gets the application configuration
244    pub fn config(&self) -> &Config {
245        &self.config
246    }
247
248    /// Updates the application configuration
249    pub fn set_config(&mut self, config: Config) {
250        self.config = config;
251        self.built = false; // Mark as needing rebuild
252    }
253}
254
255impl Default for Application {
256    fn default() -> Self {
257        Self::new()
258    }
259}
260
261/// Global error handling middleware
262async fn error_handler(
263    request: axum::extract::Request,
264    next: Next,
265) -> AxumResponse {
266    next.run(request).await
267}
268
269/// Handler for routes with path parameters
270pub async fn route_with_params_handler(
271    Path(params): Path<HashMap<String, String>>,
272    Query(query): Query<HashMap<String, String>>,
273) -> impl IntoResponse {
274    Json(json!({
275        "path_params": params,
276        "query_params": query,
277        "message": "Route with parameters handled successfully"
278    }))
279}
280
281/// Handler for JSON body requests
282pub async fn json_handler(
283    Json(payload): Json<Value>,
284) -> impl IntoResponse {
285    Json(json!({
286        "received": payload,
287        "message": "JSON payload processed successfully"
288    }))
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[tokio::test]
296    async fn test_application_creation() {
297        let app = Application::new();
298        assert!(!app.built);
299        assert!(app.axum_router.is_none());
300    }
301
302    #[tokio::test]
303    async fn test_application_build() {
304        let mut app = Application::new();
305
306        // Add a simple route
307        app.router().get("/test", || async {
308            crate::Response::ok("Test route").unwrap()
309        });
310
311        let result = app.build().await;
312        assert!(result.is_ok());
313        assert!(app.built);
314        assert!(app.axum_router.is_some());
315    }
316
317    #[test]
318    fn test_application_state() {
319        let mut app = Application::new();
320        app.set_state("test_key", "test_value");
321
322        assert_eq!(
323            app.get_state("test_key"),
324            Some(&Value::String("test_value".to_string()))
325        );
326    }
327
328    #[test]
329    fn test_application_config() {
330        let config = Config::default();
331        let app = Application::with_config(config.clone());
332        assert_eq!(app.config().app_name, config.app_name);
333    }
334}