elif_http/
controller.rs

1//! Service-Oriented Controller System
2//! 
3//! Provides a clean separation between HTTP handling (controllers) and business logic (services).
4//! Controllers are thin HTTP handlers that delegate to injected services.
5
6use std::{sync::Arc, pin::Pin, future::Future};
7use axum::{
8    extract::{State, Path, Query},
9    response::{Json, Response, IntoResponse},
10    http::StatusCode,
11};
12use serde::{Serialize, Deserialize};
13use serde_json::Value;
14
15use elif_core::Container;
16use crate::{HttpResult, HttpError, ApiResponse};
17
18/// Query parameters for pagination and filtering
19#[derive(Debug, Serialize, Deserialize)]
20pub struct QueryParams {
21    pub page: Option<u32>,
22    pub per_page: Option<u32>,
23    pub sort: Option<String>,
24    pub order: Option<String>,
25    pub filter: Option<String>,
26}
27
28impl Default for QueryParams {
29    fn default() -> Self {
30        Self {
31            page: Some(1),
32            per_page: Some(20),
33            sort: Some("id".to_string()),
34            order: Some("asc".to_string()),
35            filter: None,
36        }
37    }
38}
39
40/// Pagination metadata for API responses
41#[derive(Debug, Serialize, Deserialize)]
42pub struct PaginationMeta {
43    pub page: u32,
44    pub per_page: u32,
45    pub total: Option<u64>,
46    pub total_pages: Option<u32>,
47    pub has_more: bool,
48}
49
50/// Base controller providing HTTP utilities (no infrastructure dependencies)
51#[derive(Clone)]
52pub struct BaseController;
53
54impl BaseController {
55    pub fn new() -> Self {
56        Self
57    }
58
59    /// Validate and normalize pagination parameters
60    pub fn normalize_pagination(&self, params: &QueryParams) -> (u32, u32, u64) {
61        let page = params.page.unwrap_or(1).max(1);
62        let per_page = params.per_page.unwrap_or(20).min(100).max(1);
63        let offset = (page - 1) * per_page;
64        (page, per_page, offset as u64)
65    }
66
67    /// Create standardized success response
68    pub fn success_response<T: Serialize>(&self, data: T) -> HttpResult<Response> {
69        let api_response = ApiResponse::success(data);
70        Ok((StatusCode::OK, Json(api_response)).into_response())
71    }
72
73    /// Create standardized created response
74    pub fn created_response<T: Serialize>(&self, data: T) -> HttpResult<Response> {
75        let api_response = ApiResponse::success(data);
76        Ok((StatusCode::CREATED, Json(api_response)).into_response())
77    }
78
79    /// Create paginated response with metadata
80    pub fn paginated_response<T: Serialize>(&self, data: Vec<T>, meta: PaginationMeta) -> HttpResult<Response> {
81        let response_data = serde_json::json!({
82            "data": data,
83            "meta": meta
84        });
85        let api_response = ApiResponse::success(response_data);
86        Ok((StatusCode::OK, Json(api_response)).into_response())
87    }
88
89    /// Create standardized delete response
90    pub fn deleted_response<T: Serialize>(&self, resource_name: &str, deleted_id: Option<T>) -> HttpResult<Response> {
91        let mut response_data = serde_json::json!({
92            "message": format!("{} deleted successfully", resource_name)
93        });
94        
95        if let Some(id) = deleted_id {
96            response_data["deleted_id"] = serde_json::to_value(id)?;
97        }
98        
99        let api_response = ApiResponse::success(response_data);
100        Ok((StatusCode::OK, Json(api_response)).into_response())
101    }
102}
103
104/// Send-safe controller trait for HTTP request handling
105/// Controllers delegate business logic to injected services
106pub trait Controller: Send + Sync {
107    /// List resources with pagination
108    fn index(
109        &self,
110        container: State<Arc<Container>>,
111        params: Query<QueryParams>,
112    ) -> Pin<Box<dyn Future<Output = HttpResult<Response>> + Send>>;
113
114    /// Get single resource by ID
115    fn show(
116        &self,
117        container: State<Arc<Container>>,
118        id: Path<String>,
119    ) -> Pin<Box<dyn Future<Output = HttpResult<Response>> + Send>>;
120
121    /// Create new resource
122    fn create(
123        &self,
124        container: State<Arc<Container>>,
125        data: Json<Value>,
126    ) -> Pin<Box<dyn Future<Output = HttpResult<Response>> + Send>>;
127
128    /// Update existing resource
129    fn update(
130        &self,
131        container: State<Arc<Container>>,
132        id: Path<String>,
133        data: Json<Value>,
134    ) -> Pin<Box<dyn Future<Output = HttpResult<Response>> + Send>>;
135
136    /// Delete resource
137    fn destroy(
138        &self,
139        container: State<Arc<Container>>,
140        id: Path<String>,
141    ) -> Pin<Box<dyn Future<Output = HttpResult<Response>> + Send>>;
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use serde_json::json;
148
149    #[tokio::test]
150    async fn test_base_controller_creation() {
151        let _controller = BaseController::new();
152    }
153
154    #[tokio::test]
155    async fn test_pagination_normalization() {
156        let controller = BaseController::new();
157        let params = QueryParams {
158            page: Some(5),
159            per_page: Some(10),
160            ..Default::default()
161        };
162        
163        let (page, per_page, offset) = controller.normalize_pagination(&params);
164        assert_eq!(page, 5);
165        assert_eq!(per_page, 10);
166        assert_eq!(offset, 40);
167    }
168
169    #[tokio::test]
170    async fn test_pagination_limits() {
171        let controller = BaseController::new();
172        let params = QueryParams {
173            page: Some(0),
174            per_page: Some(200),
175            ..Default::default()
176        };
177        
178        let (page, per_page, offset) = controller.normalize_pagination(&params);
179        assert_eq!(page, 1);
180        assert_eq!(per_page, 100);
181        assert_eq!(offset, 0);
182    }
183
184    #[tokio::test]
185    async fn test_success_response_creation() {
186        let controller = BaseController::new();
187        let data = json!({"message": "test"});
188        let response = controller.success_response(data);
189        assert!(response.is_ok());
190    }
191
192    #[tokio::test]
193    async fn test_pagination_meta_creation() {
194        let meta = PaginationMeta {
195            page: 1,
196            per_page: 20,
197            total: Some(100),
198            total_pages: Some(5),
199            has_more: true,
200        };
201        
202        assert_eq!(meta.page, 1);
203        assert_eq!(meta.per_page, 20);
204        assert_eq!(meta.total, Some(100));
205    }
206}