sqlx_paginated/paginated_query_as/builders/
query_params_builder.rs

1use crate::paginated_query_as::internal::{
2    get_struct_field_names, QueryDateRangeParams, QueryPaginationParams, QuerySearchParams,
3    QuerySortParams, DEFAULT_DATE_RANGE_COLUMN_NAME, DEFAULT_MAX_PAGE_SIZE, DEFAULT_MIN_PAGE_SIZE,
4    DEFAULT_PAGE,
5};
6use crate::paginated_query_as::models::QuerySortDirection;
7use crate::QueryParams;
8use chrono::{DateTime, Utc};
9use serde::Serialize;
10use std::collections::HashMap;
11
12pub struct QueryParamsBuilder<'q, T> {
13    query: QueryParams<'q, T>,
14}
15
16impl<T: Default + Serialize> Default for QueryParamsBuilder<'_, T> {
17    fn default() -> Self {
18        Self::new()
19    }
20}
21
22impl<'q, T: Default + Serialize> QueryParamsBuilder<'q, T> {
23    /// Creates a new `QueryParamsBuilder` with default values.
24    ///
25    /// Default values include:
26    /// - Page: 1
27    /// - Page size: 10
28    /// - Sort column: "created_at"
29    /// - Sort direction: Descending
30    ///
31    /// # Examples
32    ///
33    /// ```rust
34    /// use serde::{Serialize};
35    /// use sqlx_paginated::{QueryParamsBuilder};
36    ///
37    /// #[derive(Serialize, Default)]
38    /// struct UserExample {
39    ///     name: String
40    /// }
41    /// let builder = QueryParamsBuilder::<UserExample>::new();
42    /// ```
43    pub fn new() -> Self {
44        Self {
45            query: QueryParams::default(),
46        }
47    }
48
49    /// Creates a new `QueryParamsBuilder` with default values.
50    ///
51    /// Default values include:
52    /// - Page: 1
53    /// - Page size: 10
54    /// - Sort column: "created_at"
55    /// - Sort direction: Descending
56    ///
57    /// # Examples
58    ///
59    /// ```rust
60    /// use serde::{Serialize};
61    /// use sqlx_paginated::{QueryParamsBuilder};
62    ///
63    /// #[derive(Serialize, Default)]
64    /// struct UserExample {
65    ///     name: String
66    /// }
67    /// let builder = QueryParamsBuilder::<UserExample>::new();
68    /// ```
69    pub fn with_pagination(mut self, page: i64, page_size: i64) -> Self {
70        self.query.pagination = QueryPaginationParams {
71            page: page.max(DEFAULT_PAGE),
72            page_size: page_size.clamp(DEFAULT_MIN_PAGE_SIZE, DEFAULT_MAX_PAGE_SIZE),
73        };
74        self
75    }
76
77    /// Sets sorting parameters.
78    ///
79    /// # Arguments
80    ///
81    /// * `sort_column` - Column name to sort by
82    /// * `sort_direction` - Direction of sort (Ascending or Descending)
83    ///
84    /// # Examples
85    ///
86    /// ```rust
87    /// use serde::{Serialize};
88    /// use sqlx_paginated::{QueryParamsBuilder, QuerySortDirection};
89    ///
90    /// #[derive(Serialize, Default)]
91    /// struct UserExample {
92    ///     name: String
93    /// }
94    ///
95    /// let params = QueryParamsBuilder::<UserExample>::new()
96    ///     .with_sort("updated_at", QuerySortDirection::Ascending)
97    ///     .build();
98    /// ```
99    pub fn with_sort(
100        mut self,
101        sort_column: impl Into<String>,
102        sort_direction: QuerySortDirection,
103    ) -> Self {
104        self.query.sort = QuerySortParams {
105            sort_column: sort_column.into(),
106            sort_direction,
107        };
108        self
109    }
110
111    /// Sets search parameters with multiple columns support.
112    ///
113    /// # Arguments
114    ///
115    /// * `search` - Search term to look for
116    /// * `search_columns` - Vector of column names to search in
117    ///
118    /// # Examples
119    ///
120    /// ```rust
121    /// use serde::{Serialize};
122    /// use sqlx_paginated::{QueryParamsBuilder, QuerySortDirection};
123    ///
124    /// #[derive(Serialize, Default)]
125    /// struct UserExample {
126    ///     name: String
127    /// }
128    ///
129    /// let params = QueryParamsBuilder::<UserExample>::new()
130    ///     .with_search("john", vec!["name", "email", "username"])
131    ///     .build();
132    /// ```
133    pub fn with_search(
134        mut self,
135        search: impl Into<String>,
136        search_columns: Vec<impl Into<String>>,
137    ) -> Self {
138        self.query.search = QuerySearchParams {
139            search: Some(search.into()),
140            search_columns: Some(search_columns.into_iter().map(Into::into).collect()),
141        };
142        self
143    }
144
145    /// Sets date range parameters for filtering by date.
146    ///
147    /// # Arguments
148    ///
149    /// * `date_after` - Optional start date (inclusive)
150    /// * `date_before` - Optional end date (inclusive)
151    /// * `column_name` - Optional column name to apply date range filter (defaults to created_at)
152    ///
153    /// # Examples
154    ///
155    /// ```rust
156    /// use chrono::{DateTime, Utc};
157    /// use serde::{Serialize};
158    /// use sqlx_paginated::{QueryParamsBuilder, QuerySortDirection};
159    ///
160    /// #[derive(Serialize, Default)]
161    /// struct UserExample {
162    ///     name: String,
163    ///     updated_at: DateTime<Utc>
164    /// }
165    ///
166    /// let start = DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z").unwrap().into();
167    /// let end = DateTime::parse_from_rfc3339("2024-12-31T23:59:59Z").unwrap().into();
168    ///
169    /// let params = QueryParamsBuilder::<UserExample>::new()
170    ///     .with_date_range(Some(start), Some(end), Some("updated_at"))
171    ///     .build();
172    /// ```
173    pub fn with_date_range(
174        mut self,
175        date_after: Option<DateTime<Utc>>,
176        date_before: Option<DateTime<Utc>>,
177        column_name: Option<impl Into<String>>,
178    ) -> Self {
179        self.query.date_range = QueryDateRangeParams {
180            date_after,
181            date_before,
182            date_column: column_name.map_or_else(
183                || Some(DEFAULT_DATE_RANGE_COLUMN_NAME.to_string()),
184                |column_name| Some(column_name.into()),
185            ),
186        };
187        self
188    }
189
190    /// Adds a single filter condition.
191    ///
192    /// # Arguments
193    ///
194    /// * `key` - Column name to filter on
195    /// * `value` - Optional value to filter by
196    ///
197    /// # Details
198    ///
199    /// Only adds the filter if the column exists in the model struct.
200    /// Logs a warning if tracing is enabled and the column is invalid.
201    ///
202    /// # Examples
203    ///
204    /// ```rust
205    /// use std::any::Any;
206    /// use serde::{Serialize};
207    /// use sqlx_paginated::{QueryParamsBuilder};
208    ///
209    /// #[derive(Serialize, Default)]
210    /// struct UserExample {
211    ///     name: String,
212    ///     status: String,
213    ///     role: String
214    /// }
215    ///
216    /// let params = QueryParamsBuilder::<UserExample>::new()
217    ///     .with_filter("status", Some("active"))
218    ///     .with_filter("role", Some("admin"))
219    ///     .build();
220    /// ```
221    pub fn with_filter(mut self, key: impl Into<String>, value: Option<impl Into<String>>) -> Self {
222        let key = key.into();
223        let valid_fields = get_struct_field_names::<T>();
224
225        if valid_fields.contains(&key) {
226            self.query.filters.insert(key, value.map(Into::into));
227        } else {
228            #[cfg(feature = "tracing")]
229            tracing::warn!(column = %key, "Skipping invalid filter column");
230        }
231        self
232    }
233
234    /// Adds multiple filter conditions from a HashMap.
235    ///
236    /// # Arguments
237    ///
238    /// * `filters` - HashMap of column names and their filter values
239    ///
240    /// # Details
241    ///
242    /// Only adds filters for columns that exist in the model struct.
243    /// Logs a warning if tracing is enabled and a column is invalid.
244    ///
245    /// # Examples
246    ///
247    /// ```rust
248    /// use std::collections::HashMap;
249    /// use serde::{Serialize};
250    /// use sqlx_paginated::{QueryParamsBuilder};
251    ///
252    /// #[derive(Serialize, Default)]
253    /// struct UserExample {
254    ///     name: String,
255    ///     status: String,
256    ///     role: String
257    /// }
258    ///
259    /// let mut filters = HashMap::new();
260    /// filters.insert("status", Some("active"));
261    /// filters.insert("role", Some("admin"));
262    ///
263    /// let params = QueryParamsBuilder::<UserExample>::new()
264    ///     .with_filters(filters)
265    ///     .build();
266    /// ```
267    pub fn with_filters(
268        mut self,
269        filters: HashMap<impl Into<String>, Option<impl Into<String>>>,
270    ) -> Self {
271        let valid_fields = get_struct_field_names::<T>();
272
273        self.query
274            .filters
275            .extend(filters.into_iter().filter_map(|(key, value)| {
276                let key = key.into();
277                if valid_fields.contains(&key) {
278                    Some((key, value.map(Into::into)))
279                } else {
280                    #[cfg(feature = "tracing")]
281                    tracing::warn!(column = %key, "Skipping invalid filter column");
282                    None
283                }
284            }));
285
286        self
287    }
288
289    /// Builds and returns the final QueryParams.
290    ///
291    /// # Returns
292    ///
293    /// Returns the constructed `QueryParams<T>` with all the configured parameters.
294    ///
295    /// # Examples
296    ///
297    /// ```rust
298    /// use chrono::{DateTime, Utc};
299    /// use sqlx_paginated::{QueryParamsBuilder, QuerySortDirection};
300    /// use serde::{Serialize};
301    ///
302    /// #[derive(Serialize, Default)]
303    /// struct UserExample {
304    ///     name: String,
305    ///     status: String,
306    ///     email: String,
307    ///     created_at: DateTime<Utc>
308    /// }
309    ///
310    /// let params = QueryParamsBuilder::<UserExample>::new()
311    ///     .with_pagination(1, 20)
312    ///     .with_sort("created_at", QuerySortDirection::Descending)
313    ///     .with_search("john", vec!["name", "email"])
314    ///     .with_filter("status", Some("active"))
315    ///     .build();
316    /// ```
317    pub fn build(self) -> QueryParams<'q, T> {
318        self.query
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325    use crate::paginated_query_as::internal::{
326        DEFAULT_SEARCH_COLUMN_NAMES, DEFAULT_SORT_COLUMN_NAME,
327    };
328    use crate::paginated_query_as::models::QuerySortDirection;
329    use chrono::{DateTime, Utc};
330    use std::collections::HashMap;
331
332    #[derive(Debug, Default, Serialize)]
333    struct TestModel {
334        name: String,
335        title: String,
336        description: String,
337        status: String,
338        category: String,
339        updated_at: DateTime<Utc>,
340        created_at: DateTime<Utc>,
341    }
342
343    #[test]
344    fn test_pagination_defaults() {
345        let params = QueryParamsBuilder::<TestModel>::new().build();
346
347        assert_eq!(
348            params.pagination.page_size, DEFAULT_MIN_PAGE_SIZE,
349            "Default page size should be {}",
350            DEFAULT_MIN_PAGE_SIZE
351        );
352        assert_eq!(
353            params.pagination.page, DEFAULT_PAGE,
354            "Default page should be {}",
355            DEFAULT_PAGE
356        );
357
358        // Test page size clamping
359        let params = QueryParamsBuilder::<TestModel>::new()
360            .with_pagination(1, DEFAULT_MAX_PAGE_SIZE + 10)
361            .build();
362
363        assert_eq!(
364            params.pagination.page_size, DEFAULT_MAX_PAGE_SIZE,
365            "Page size should be clamped to maximum {}",
366            DEFAULT_MAX_PAGE_SIZE
367        );
368
369        let params = QueryParamsBuilder::<TestModel>::new()
370            .with_pagination(1, DEFAULT_MIN_PAGE_SIZE - 5)
371            .build();
372
373        assert_eq!(
374            params.pagination.page_size, DEFAULT_MIN_PAGE_SIZE,
375            "Page size should be clamped to minimum {}",
376            DEFAULT_MIN_PAGE_SIZE
377        );
378    }
379
380    #[test]
381    fn test_default_sort_column() {
382        let params = QueryParamsBuilder::<TestModel>::new().build();
383
384        assert_eq!(
385            params.sort.sort_column, DEFAULT_SORT_COLUMN_NAME,
386            "Default sort column should be '{}'",
387            DEFAULT_SORT_COLUMN_NAME
388        );
389    }
390
391    #[test]
392    fn test_date_range_defaults() {
393        let params = QueryParamsBuilder::<TestModel>::new().build();
394
395        assert_eq!(
396            params.date_range.date_column,
397            Some(DEFAULT_DATE_RANGE_COLUMN_NAME.to_string()),
398            "Default date range column should be '{}'",
399            DEFAULT_DATE_RANGE_COLUMN_NAME
400        );
401        assert!(
402            params.date_range.date_after.is_none(),
403            "Default date_after should be None"
404        );
405        assert!(
406            params.date_range.date_before.is_none(),
407            "Default date_before should be None"
408        );
409    }
410
411    #[test]
412    fn test_search_defaults() {
413        let params = QueryParamsBuilder::<TestModel>::new().build();
414
415        // Check if default search columns are set
416        assert_eq!(
417            params.search.search_columns,
418            Some(
419                DEFAULT_SEARCH_COLUMN_NAMES
420                    .iter()
421                    .map(|&s| s.to_string())
422                    .collect()
423            ),
424            "Default search columns should be {:?}",
425            DEFAULT_SEARCH_COLUMN_NAMES
426        );
427        assert!(
428            params.search.search.is_none(),
429            "Default search term should be None"
430        );
431    }
432
433    #[test]
434    fn test_combined_defaults() {
435        let params = QueryParamsBuilder::<TestModel>::new().build();
436
437        // Test all defaults together
438        assert_eq!(params.pagination.page, DEFAULT_PAGE);
439        assert_eq!(params.pagination.page_size, DEFAULT_MIN_PAGE_SIZE);
440        assert_eq!(params.sort.sort_column, DEFAULT_SORT_COLUMN_NAME);
441        assert_eq!(params.sort.sort_direction, QuerySortDirection::Descending);
442        assert_eq!(
443            params.date_range.date_column,
444            Some(DEFAULT_DATE_RANGE_COLUMN_NAME.to_string())
445        );
446        assert_eq!(
447            params.search.search_columns,
448            Some(
449                DEFAULT_SEARCH_COLUMN_NAMES
450                    .iter()
451                    .map(|&s| s.to_string())
452                    .collect()
453            )
454        );
455        assert!(params.search.search.is_none());
456        assert!(params.date_range.date_after.is_none());
457        assert!(params.date_range.date_before.is_none());
458    }
459
460    #[test]
461    fn test_empty_params() {
462        let params = QueryParamsBuilder::<TestModel>::new().build();
463
464        assert_eq!(params.pagination.page, 1);
465        assert_eq!(params.pagination.page_size, 10);
466        assert_eq!(params.sort.sort_column, "created_at");
467        assert!(matches!(
468            params.sort.sort_direction,
469            QuerySortDirection::Descending
470        ));
471    }
472
473    #[test]
474    fn test_partial_params() {
475        let params = QueryParamsBuilder::<TestModel>::new()
476            .with_pagination(2, 10)
477            .with_search("test".to_string(), vec!["name".to_string()])
478            .build();
479
480        assert_eq!(params.pagination.page, 2);
481        assert_eq!(params.search.search, Some("test".to_string()));
482        assert_eq!(params.pagination.page_size, 10);
483        assert_eq!(params.sort.sort_column, "created_at");
484        assert!(matches!(
485            params.sort.sort_direction,
486            QuerySortDirection::Descending
487        ));
488    }
489
490    #[test]
491    fn test_invalid_params() {
492        // For builder pattern, invalid params would be handled at compile time
493        // But we can test the defaults
494        let params = QueryParamsBuilder::<TestModel>::new()
495            .with_pagination(0, 0) // Should be clamped to minimum values
496            .build();
497
498        assert_eq!(params.pagination.page, 1);
499        assert_eq!(params.pagination.page_size, 10);
500    }
501
502    #[test]
503    fn test_filters() {
504        let mut filters = HashMap::new();
505        filters.insert("status".to_string(), Some("active".to_string()));
506        filters.insert("category".to_string(), Some("test".to_string()));
507
508        let params = QueryParamsBuilder::<TestModel>::new()
509            .with_filters(filters)
510            .build();
511
512        assert!(params.filters.contains_key("status"));
513        assert_eq!(
514            params.filters.get("status").unwrap(),
515            &Some("active".to_string())
516        );
517        assert!(params.filters.contains_key("category"));
518        assert_eq!(
519            params.filters.get("category").unwrap(),
520            &Some("test".to_string())
521        );
522    }
523
524    #[test]
525    fn test_search_with_columns() {
526        let params = QueryParamsBuilder::<TestModel>::new()
527            .with_search(
528                "test".to_string(),
529                vec!["title".to_string(), "description".to_string()],
530            )
531            .build();
532
533        assert_eq!(params.search.search, Some("test".to_string()));
534        assert_eq!(
535            params.search.search_columns,
536            Some(vec!["title".to_string(), "description".to_string()])
537        );
538    }
539
540    #[test]
541    fn test_full_params() {
542        let params = QueryParamsBuilder::<TestModel>::new()
543            .with_pagination(2, 20)
544            .with_sort("updated_at".to_string(), QuerySortDirection::Ascending)
545            .with_search(
546                "test".to_string(),
547                vec!["title".to_string(), "description".to_string()],
548            )
549            .with_date_range(Some(Utc::now()), None, None::<String>)
550            .build();
551
552        assert_eq!(params.pagination.page, 2);
553        assert_eq!(params.pagination.page_size, 20);
554        assert_eq!(params.sort.sort_column, "updated_at");
555        assert!(matches!(
556            params.sort.sort_direction,
557            QuerySortDirection::Ascending
558        ));
559        assert_eq!(params.search.search, Some("test".to_string()));
560        assert_eq!(
561            params.search.search_columns,
562            Some(vec!["title".to_string(), "description".to_string()])
563        );
564        assert!(params.date_range.date_after.is_some());
565        assert!(params.date_range.date_before.is_none());
566    }
567
568    #[test]
569    fn test_filter_chain() {
570        let params = QueryParamsBuilder::<TestModel>::new()
571            .with_filter("status", Some("active"))
572            .with_filter("category", Some("test"))
573            .build();
574
575        assert_eq!(
576            params.filters.get("status").unwrap(),
577            &Some("active".to_string())
578        );
579        assert_eq!(
580            params.filters.get("category").unwrap(),
581            &Some("test".to_string())
582        );
583    }
584
585    #[test]
586    fn test_mixed_pagination() {
587        let params = QueryParamsBuilder::<TestModel>::new()
588            .with_pagination(2, 10)
589            .with_search("test".to_string(), vec!["title".to_string()])
590            .with_filter("status", Some("active"))
591            .build();
592
593        assert_eq!(params.pagination.page, 2);
594        assert_eq!(params.pagination.page_size, 10);
595        assert_eq!(params.search.search, Some("test".to_string()));
596        assert_eq!(
597            params.filters.get("status").unwrap(),
598            &Some("active".to_string())
599        );
600    }
601}