elif_http/request/
extractors.rs

1//! Request data extractors and Simple input helpers
2
3use crate::errors::{HttpError, HttpResult};
4use crate::request::ElifRequest;
5use serde::de::DeserializeOwned;
6use std::collections::HashMap;
7use std::str::FromStr;
8
9/// Framework-native Query extractor - use instead of axum::extract::Query
10#[derive(Debug)]
11pub struct ElifQuery<T>(pub T);
12
13impl<T: DeserializeOwned> ElifQuery<T> {
14    /// Extract and deserialize query parameters from request
15    pub fn from_request(request: &ElifRequest) -> HttpResult<Self> {
16        let query_str = request.query_string().unwrap_or("");
17        let data = serde_urlencoded::from_str::<T>(query_str)
18            .map_err(|e| HttpError::bad_request(format!("Invalid query parameters: {}", e)))?;
19        Ok(ElifQuery(data))
20    }
21}
22
23/// Framework-native Path extractor - use instead of axum::extract::Path  
24#[derive(Debug)]
25pub struct ElifPath<T>(pub T);
26
27impl<T: DeserializeOwned> ElifPath<T> {
28    /// Extract and deserialize path parameters from request
29    pub fn from_request(request: &ElifRequest) -> HttpResult<Self> {
30        let data = request.path_params::<T>()?;
31        Ok(ElifPath(data))
32    }
33}
34
35/// Framework-native State extractor - use instead of axum::extract::State
36#[derive(Debug)]
37pub struct ElifState<T>(pub T);
38
39impl<T: Clone> ElifState<T> {
40    /// Extract state from application context
41    pub fn new(state: T) -> Self {
42        ElifState(state)
43    }
44
45    /// Get reference to inner state
46    pub fn inner(&self) -> &T {
47        &self.0
48    }
49
50    /// Get owned copy of inner state (requires Clone)
51    pub fn into_inner(self) -> T {
52        self.0
53    }
54}
55
56// Simple input helpers for ElifRequest
57impl ElifRequest {
58    /// Simple input extraction with default value
59    ///
60    /// Searches query parameters first, then path parameters.
61    /// Returns the default value if the parameter is missing or can't be parsed.
62    ///
63    /// Simple equivalent: `$request->input('page', 1)`
64    pub fn input<T>(&self, key: &str, default: T) -> T
65    where
66        T: FromStr + Clone,
67        T::Err: std::fmt::Debug,
68    {
69        self.query_param(key)
70            .or_else(|| self.path_param(key))
71            .and_then(|s| s.parse().ok())
72            .unwrap_or(default)
73    }
74
75    /// Simple input extraction that returns Option
76    ///
77    /// Simple equivalent: `$request->input('search')`
78    pub fn input_optional<T>(&self, key: &str) -> Option<T>
79    where
80        T: FromStr,
81        T::Err: std::fmt::Debug,
82    {
83        self.query_param(key)
84            .or_else(|| self.path_param(key))
85            .and_then(|s| s.parse().ok())
86    }
87
88    /// Extract a string input with default
89    ///
90    /// Simple equivalent: `$request->input('name', 'default')`
91    pub fn string(&self, key: &str, default: &str) -> String {
92        self.query_param(key)
93            .or_else(|| self.path_param(key))
94            .cloned()
95            .unwrap_or_else(|| default.to_string())
96    }
97
98    /// Extract an optional string input
99    ///
100    /// Simple equivalent: `$request->input('search')`
101    pub fn string_optional(&self, key: &str) -> Option<String> {
102        self.query_param(key)
103            .or_else(|| self.path_param(key))
104            .cloned()
105    }
106
107    /// Extract an integer input with default
108    ///
109    /// Simple equivalent: `$request->input('page', 1)`
110    pub fn integer(&self, key: &str, default: i64) -> i64 {
111        self.input(key, default)
112    }
113
114    /// Extract an optional integer input
115    ///
116    /// Simple equivalent: `$request->input('limit')`
117    pub fn integer_optional(&self, key: &str) -> Option<i64> {
118        self.input_optional(key)
119    }
120
121    /// Extract a boolean input with default
122    ///
123    /// Recognizes: "true", "1", "on", "yes" as true (case-insensitive)
124    /// Simple equivalent: `$request->boolean('active', false)`
125    pub fn boolean(&self, key: &str, default: bool) -> bool {
126        self.query_param(key)
127            .or_else(|| self.path_param(key))
128            .map(|s| match s.to_lowercase().as_str() {
129                "true" | "1" | "on" | "yes" => true,
130                "false" | "0" | "off" | "no" => false,
131                _ => default,
132            })
133            .unwrap_or(default)
134    }
135
136    /// Extract multiple inputs at once as a HashMap
137    ///
138    /// Simple equivalent: `$request->only(['name', 'email', 'age'])`
139    pub fn inputs(&self, keys: &[&str]) -> HashMap<String, String> {
140        keys.iter()
141            .filter_map(|&key| {
142                self.query_param(key)
143                    .or_else(|| self.path_param(key))
144                    .map(|val| (key.to_string(), val.clone()))
145            })
146            .collect()
147    }
148
149    /// Extract all query parameters as HashMap
150    ///
151    /// Simple equivalent: `$request->query()`
152    pub fn all_query(&self) -> HashMap<String, String> {
153        self.query_params
154            .iter()
155            .map(|(k, v)| (k.clone(), v.clone()))
156            .collect()
157    }
158
159    /// Check if a parameter exists (in query or path)
160    ///
161    /// Simple equivalent: `$request->has('search')`
162    pub fn has(&self, key: &str) -> bool {
163        self.query_param(key).is_some() || self.path_param(key).is_some()
164    }
165
166    /// Check if a parameter exists and is not empty
167    ///
168    /// Simple equivalent: `$request->filled('search')`
169    pub fn filled(&self, key: &str) -> bool {
170        self.query_param(key)
171            .or_else(|| self.path_param(key))
172            .map(|s| !s.trim().is_empty())
173            .unwrap_or(false)
174    }
175
176    /// Get a parameter as array (comma-separated or multiple values)
177    ///
178    /// Simple equivalent: `$request->input('tags', [])`
179    pub fn array(&self, key: &str) -> Vec<String> {
180        if let Some(value) = self.query_param(key).or_else(|| self.path_param(key)) {
181            // Split by comma and clean up
182            value
183                .split(',')
184                .map(|s| s.trim().to_string())
185                .filter(|s| !s.is_empty())
186                .collect()
187        } else {
188            Vec::new()
189        }
190    }
191
192    /// Extract pagination parameters with sensible defaults
193    ///
194    /// Returns (page, per_page) with defaults of (1, 10)
195    /// Simple equivalent: `[$page, $perPage] = [$request->input('page', 1), $request->input('per_page', 10)]`
196    pub fn pagination(&self) -> (u32, u32) {
197        let page = self.input("page", 1u32).max(1);
198        let per_page = self.input("per_page", 10u32).clamp(1, 100);
199        (page, per_page)
200    }
201
202    /// Extract sorting parameters
203    ///
204    /// Returns (sort_field, sort_direction) with defaults
205    /// Simple equivalent: `[$sort, $order] = [$request->input('sort', 'id'), $request->input('order', 'asc')]`
206    pub fn sorting(&self, default_field: &str) -> (String, String) {
207        let sort = self.string("sort", default_field);
208        let order = self.string("order", "asc");
209        let direction = match order.to_lowercase().as_str() {
210            "desc" | "descending" | "down" => "desc".to_string(),
211            _ => "asc".to_string(),
212        };
213        (sort, direction)
214    }
215
216    /// Extract search and filtering parameters
217    ///
218    /// Returns a HashMap of common filter parameters
219    /// Simple equivalent: `$filters = $request->only(['search', 'status', 'category'])`
220    pub fn filters(&self) -> HashMap<String, String> {
221        self.inputs(&[
222            "search", "q", "query", "status", "state", "category", "type", "filter", "filters",
223        ])
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use crate::request::ElifRequest;
231    use serde::Deserialize;
232
233    #[derive(Debug, Deserialize, PartialEq)]
234    struct TestQuery {
235        name: String,
236        age: Option<u32>,
237        active: Option<bool>,
238    }
239
240    #[derive(Debug, Deserialize, PartialEq)]
241    struct TestPath {
242        id: u32,
243        slug: String,
244    }
245
246    #[derive(Debug, Clone, PartialEq)]
247    struct TestAppState {
248        database_url: String,
249        api_key: String,
250    }
251
252    fn create_test_request_with_query(query: &str) -> ElifRequest {
253        use axum::body::Body;
254        use axum::extract::Request;
255
256        let uri = if query.is_empty() {
257            "/test".to_string()
258        } else {
259            format!("/test?{}", query)
260        };
261
262        let request = Request::builder()
263            .method("GET")
264            .uri(uri)
265            .body(Body::empty())
266            .unwrap();
267
268        let (parts, _body) = request.into_parts();
269        ElifRequest::extract_elif_request(
270            crate::request::ElifMethod::from_axum(parts.method),
271            parts.uri,
272            crate::response::headers::ElifHeaderMap::from_axum(parts.headers),
273            None,
274        )
275    }
276
277    #[test]
278    fn test_elif_query_extraction_success() {
279        let request = create_test_request_with_query("name=John&age=30&active=true");
280        let result: Result<ElifQuery<TestQuery>, _> = ElifQuery::from_request(&request);
281
282        assert!(result.is_ok());
283        let query = result.unwrap();
284        assert_eq!(query.0.name, "John");
285        assert_eq!(query.0.age, Some(30));
286        assert_eq!(query.0.active, Some(true));
287    }
288
289    #[test]
290    fn test_elif_query_extraction_partial() {
291        let request = create_test_request_with_query("name=Alice");
292        let result: Result<ElifQuery<TestQuery>, _> = ElifQuery::from_request(&request);
293
294        assert!(result.is_ok());
295        let query = result.unwrap();
296        assert_eq!(query.0.name, "Alice");
297        assert_eq!(query.0.age, None);
298        assert_eq!(query.0.active, None);
299    }
300
301    #[test]
302    fn test_elif_query_extraction_empty() {
303        let request = create_test_request_with_query("");
304        let result: Result<ElifQuery<TestQuery>, _> = ElifQuery::from_request(&request);
305
306        // Should fail because 'name' is required
307        assert!(result.is_err());
308        assert!(matches!(result.unwrap_err(), HttpError::BadRequest { .. }));
309    }
310
311    #[test]
312    fn test_elif_query_extraction_invalid_format() {
313        let request = create_test_request_with_query("name=John&age=not_a_number");
314        let result: Result<ElifQuery<TestQuery>, _> = ElifQuery::from_request(&request);
315
316        assert!(result.is_err());
317        assert!(matches!(result.unwrap_err(), HttpError::BadRequest { .. }));
318    }
319
320    #[test]
321    fn test_elif_query_url_decoding() {
322        let request = create_test_request_with_query("name=John%20Doe&active=true");
323        let result: Result<ElifQuery<TestQuery>, _> = ElifQuery::from_request(&request);
324
325        assert!(result.is_ok());
326        let query = result.unwrap();
327        assert_eq!(query.0.name, "John Doe");
328    }
329
330    #[test]
331    fn test_elif_state_creation_and_access() {
332        let state = TestAppState {
333            database_url: "postgres://localhost:5432/test".to_string(),
334            api_key: "secret_key_123".to_string(),
335        };
336
337        let elif_state = ElifState::new(state.clone());
338
339        // Test inner reference
340        assert_eq!(elif_state.inner(), &state);
341
342        // Test into_inner
343        let recovered_state = elif_state.into_inner();
344        assert_eq!(recovered_state, state);
345    }
346
347    #[test]
348    fn test_elif_state_clone_requirement() {
349        #[derive(Clone, PartialEq, Debug)]
350        struct CloneableState {
351            value: i32,
352        }
353
354        let state = CloneableState { value: 42 };
355        let elif_state = ElifState::new(state.clone());
356
357        assert_eq!(elif_state.inner().value, 42);
358        assert_eq!(elif_state.into_inner(), state);
359    }
360
361    #[test]
362    fn test_elif_query_debug_impl() {
363        let query = ElifQuery(TestQuery {
364            name: "Test".to_string(),
365            age: Some(25),
366            active: Some(false),
367        });
368
369        let debug_string = format!("{:?}", query);
370        assert!(debug_string.contains("ElifQuery"));
371        assert!(debug_string.contains("Test"));
372    }
373
374    #[test]
375    fn test_elif_path_debug_impl() {
376        let path = ElifPath(TestPath {
377            id: 123,
378            slug: "test-slug".to_string(),
379        });
380
381        let debug_string = format!("{:?}", path);
382        assert!(debug_string.contains("ElifPath"));
383        assert!(debug_string.contains("123"));
384        assert!(debug_string.contains("test-slug"));
385    }
386
387    #[test]
388    fn test_elif_state_debug_impl() {
389        let state = ElifState::new(TestAppState {
390            database_url: "postgres://localhost:5432/test".to_string(),
391            api_key: "secret_key_123".to_string(),
392        });
393
394        let debug_string = format!("{:?}", state);
395        assert!(debug_string.contains("ElifState"));
396    }
397}