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::paginated_query_as::models::{QueryFilterCondition, QueryFilterOperator};
8use crate::QueryParams;
9use chrono::{DateTime, Utc};
10use serde::Serialize;
11use std::collections::HashMap;
12
13pub struct QueryParamsBuilder<'q, T> {
14 query: QueryParams<'q, T>,
15}
16
17impl<T: Default + Serialize> Default for QueryParamsBuilder<'_, T> {
18 fn default() -> Self {
19 Self::new()
20 }
21}
22
23impl<'q, T: Default + Serialize> QueryParamsBuilder<'q, T> {
24 /// Creates a new `QueryParamsBuilder` with default values.
25 ///
26 /// Default values include:
27 /// - Page: 1
28 /// - Page size: 10
29 /// - Sort column: "created_at"
30 /// - Sort direction: Descending
31 ///
32 /// # Examples
33 ///
34 /// ```rust
35 /// use serde::{Serialize};
36 /// use sqlx_paginated::{QueryParamsBuilder};
37 ///
38 /// #[derive(Serialize, Default)]
39 /// struct UserExample {
40 /// name: String
41 /// }
42 /// let builder = QueryParamsBuilder::<UserExample>::new();
43 /// ```
44 pub fn new() -> Self {
45 Self {
46 query: QueryParams::default(),
47 }
48 }
49
50 /// Creates a new `QueryParamsBuilder` with default values.
51 ///
52 /// Default values include:
53 /// - Page: 1
54 /// - Page size: 10
55 /// - Sort column: "created_at"
56 /// - Sort direction: Descending
57 ///
58 /// # Examples
59 ///
60 /// ```rust
61 /// use serde::{Serialize};
62 /// use sqlx_paginated::{QueryParamsBuilder};
63 ///
64 /// #[derive(Serialize, Default)]
65 /// struct UserExample {
66 /// name: String
67 /// }
68 /// let builder = QueryParamsBuilder::<UserExample>::new();
69 /// ```
70 pub fn with_pagination(mut self, page: i64, page_size: i64) -> Self {
71 self.query.pagination = QueryPaginationParams {
72 page: page.max(DEFAULT_PAGE),
73 page_size: page_size.clamp(DEFAULT_MIN_PAGE_SIZE, DEFAULT_MAX_PAGE_SIZE),
74 };
75 self
76 }
77
78 /// Sets sorting parameters.
79 ///
80 /// # Arguments
81 ///
82 /// * `sort_column` - Column name to sort by
83 /// * `sort_direction` - Direction of sort (Ascending or Descending)
84 ///
85 /// # Examples
86 ///
87 /// ```rust
88 /// use serde::{Serialize};
89 /// use sqlx_paginated::{QueryParamsBuilder, QuerySortDirection};
90 ///
91 /// #[derive(Serialize, Default)]
92 /// struct UserExample {
93 /// name: String
94 /// }
95 ///
96 /// let params = QueryParamsBuilder::<UserExample>::new()
97 /// .with_sort("updated_at", QuerySortDirection::Ascending)
98 /// .build();
99 /// ```
100 pub fn with_sort(
101 mut self,
102 sort_column: impl Into<String>,
103 sort_direction: QuerySortDirection,
104 ) -> Self {
105 self.query.sort = QuerySortParams {
106 sort_column: sort_column.into(),
107 sort_direction,
108 };
109 self
110 }
111
112 /// Sets search parameters with multiple columns support.
113 ///
114 /// # Arguments
115 ///
116 /// * `search` - Search term to look for
117 /// * `search_columns` - Vector of column names to search in
118 ///
119 /// # Examples
120 ///
121 /// ```rust
122 /// use serde::{Serialize};
123 /// use sqlx_paginated::{QueryParamsBuilder, QuerySortDirection};
124 ///
125 /// #[derive(Serialize, Default)]
126 /// struct UserExample {
127 /// name: String
128 /// }
129 ///
130 /// let params = QueryParamsBuilder::<UserExample>::new()
131 /// .with_search("john", vec!["name", "email", "username"])
132 /// .build();
133 /// ```
134 pub fn with_search(
135 mut self,
136 search: impl Into<String>,
137 search_columns: Vec<impl Into<String>>,
138 ) -> Self {
139 self.query.search = QuerySearchParams {
140 search: Some(search.into()),
141 search_columns: Some(search_columns.into_iter().map(Into::into).collect()),
142 };
143 self
144 }
145
146 /// Sets date range parameters for filtering by date.
147 ///
148 /// # Arguments
149 ///
150 /// * `date_after` - Optional start date (inclusive)
151 /// * `date_before` - Optional end date (inclusive)
152 /// * `column_name` - Optional column name to apply date range filter (defaults to created_at)
153 ///
154 /// # Examples
155 ///
156 /// ```rust
157 /// use chrono::{DateTime, Utc};
158 /// use serde::{Serialize};
159 /// use sqlx_paginated::{QueryParamsBuilder, QuerySortDirection};
160 ///
161 /// #[derive(Serialize, Default)]
162 /// struct UserExample {
163 /// name: String,
164 /// updated_at: DateTime<Utc>
165 /// }
166 ///
167 /// let start = DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z").unwrap().into();
168 /// let end = DateTime::parse_from_rfc3339("2024-12-31T23:59:59Z").unwrap().into();
169 ///
170 /// let params = QueryParamsBuilder::<UserExample>::new()
171 /// .with_date_range(Some(start), Some(end), Some("updated_at"))
172 /// .build();
173 /// ```
174 pub fn with_date_range(
175 mut self,
176 date_after: Option<DateTime<Utc>>,
177 date_before: Option<DateTime<Utc>>,
178 column_name: Option<impl Into<String>>,
179 ) -> Self {
180 self.query.date_range = QueryDateRangeParams {
181 date_after,
182 date_before,
183 date_column: column_name.map_or_else(
184 || Some(DEFAULT_DATE_RANGE_COLUMN_NAME.to_string()),
185 |column_name| Some(column_name.into()),
186 ),
187 };
188 self
189 }
190
191 /// Adds a single filter condition with an operator.
192 ///
193 /// # Arguments
194 ///
195 /// * `key` - Column name to filter on
196 /// * `operator` - The comparison operator to use
197 /// * `value` - Value to filter by (required for most operators except IS NULL/IS NOT NULL)
198 ///
199 /// # Details
200 ///
201 /// Only adds the filter if the column exists in the model struct.
202 /// Logs a warning if tracing is enabled and the column is invalid.
203 ///
204 /// # Examples
205 ///
206 /// ```rust
207 /// use serde::{Serialize};
208 /// use sqlx_paginated::{QueryParamsBuilder, QueryFilterOperator};
209 ///
210 /// #[derive(Serialize, Default)]
211 /// struct Product {
212 /// name: String,
213 /// price: f64,
214 /// stock: i32,
215 /// status: String,
216 /// }
217 ///
218 /// let params = QueryParamsBuilder::<Product>::new()
219 /// .with_filter_operator("price", QueryFilterOperator::GreaterThan, "10.00")
220 /// .with_filter_operator("stock", QueryFilterOperator::LessOrEqual, "100")
221 /// .with_filter_operator("status", QueryFilterOperator::NotEqual, "deleted")
222 /// .build();
223 /// ```
224 pub fn with_filter_operator(
225 mut self,
226 key: impl Into<String>,
227 operator: QueryFilterOperator,
228 value: impl Into<String>,
229 ) -> Self {
230 let key = key.into();
231 let valid_fields = get_struct_field_names::<T>();
232
233 if valid_fields.contains(&key) {
234 self.query
235 .filters
236 .insert(key, QueryFilterCondition::new(operator, Some(value)));
237 } else {
238 #[cfg(feature = "tracing")]
239 tracing::warn!(column = %key, "Skipping invalid filter column");
240 }
241 self
242 }
243
244 /// Adds a filter condition for IS NULL or IS NOT NULL checks.
245 ///
246 /// # Arguments
247 ///
248 /// * `key` - Column name to filter on
249 /// * `is_null` - If true, checks IS NULL; if false, checks IS NOT NULL
250 ///
251 /// # Examples
252 ///
253 /// ```rust
254 /// use serde::{Serialize};
255 /// use sqlx_paginated::{QueryParamsBuilder};
256 ///
257 /// #[derive(Serialize, Default)]
258 /// struct User {
259 /// name: String,
260 /// deleted_at: Option<String>,
261 /// }
262 ///
263 /// let params = QueryParamsBuilder::<User>::new()
264 /// .with_filter_null("deleted_at", true) // IS NULL
265 /// .build();
266 /// ```
267 pub fn with_filter_null(mut self, key: impl Into<String>, is_null: bool) -> Self {
268 let key = key.into();
269 let valid_fields = get_struct_field_names::<T>();
270
271 if valid_fields.contains(&key) {
272 let condition = if is_null {
273 QueryFilterCondition::is_null()
274 } else {
275 QueryFilterCondition::is_not_null()
276 };
277 self.query.filters.insert(key, condition);
278 } else {
279 #[cfg(feature = "tracing")]
280 tracing::warn!(column = %key, "Skipping invalid filter column");
281 }
282 self
283 }
284
285 /// Adds an IN filter condition with multiple values.
286 ///
287 /// # Arguments
288 ///
289 /// * `key` - Column name to filter on
290 /// * `values` - Vector of values to check against
291 ///
292 /// # Examples
293 ///
294 /// ```rust
295 /// use serde::{Serialize};
296 /// use sqlx_paginated::{QueryParamsBuilder};
297 ///
298 /// #[derive(Serialize, Default)]
299 /// struct User {
300 /// name: String,
301 /// role: String,
302 /// }
303 ///
304 /// let params = QueryParamsBuilder::<User>::new()
305 /// .with_filter_in("role", vec!["admin", "moderator", "user"])
306 /// .build();
307 /// ```
308 pub fn with_filter_in(
309 mut self,
310 key: impl Into<String>,
311 values: Vec<impl Into<String>>,
312 ) -> Self {
313 let key = key.into();
314 let valid_fields = get_struct_field_names::<T>();
315
316 if valid_fields.contains(&key) {
317 self.query
318 .filters
319 .insert(key, QueryFilterCondition::in_list(values));
320 } else {
321 #[cfg(feature = "tracing")]
322 tracing::warn!(column = %key, "Skipping invalid filter column");
323 }
324 self
325 }
326
327 /// Adds a NOT IN filter condition with multiple values.
328 ///
329 /// # Arguments
330 ///
331 /// * `key` - Column name to filter on
332 /// * `values` - Vector of values to exclude
333 ///
334 /// # Examples
335 ///
336 /// ```rust
337 /// use serde::{Serialize};
338 /// use sqlx_paginated::{QueryParamsBuilder};
339 ///
340 /// #[derive(Serialize, Default)]
341 /// struct User {
342 /// name: String,
343 /// role: String,
344 /// }
345 ///
346 /// let params = QueryParamsBuilder::<User>::new()
347 /// .with_filter_not_in("role", vec!["banned", "suspended"])
348 /// .build();
349 /// ```
350 pub fn with_filter_not_in(
351 mut self,
352 key: impl Into<String>,
353 values: Vec<impl Into<String>>,
354 ) -> Self {
355 let key = key.into();
356 let valid_fields = get_struct_field_names::<T>();
357
358 if valid_fields.contains(&key) {
359 self.query
360 .filters
361 .insert(key, QueryFilterCondition::not_in_list(values));
362 } else {
363 #[cfg(feature = "tracing")]
364 tracing::warn!(column = %key, "Skipping invalid filter column");
365 }
366 self
367 }
368
369 /// Adds a simple equality filter condition (backward compatible).
370 ///
371 /// # Arguments
372 ///
373 /// * `key` - Column name to filter on
374 /// * `value` - Optional value to filter by
375 ///
376 /// # Details
377 ///
378 /// Only adds the filter if the column exists in the model struct.
379 /// Logs a warning if tracing is enabled and the column is invalid.
380 /// This method maintains backward compatibility with the original API.
381 ///
382 /// # Examples
383 ///
384 /// ```rust
385 /// use std::any::Any;
386 /// use serde::{Serialize};
387 /// use sqlx_paginated::{QueryParamsBuilder};
388 ///
389 /// #[derive(Serialize, Default)]
390 /// struct UserExample {
391 /// name: String,
392 /// status: String,
393 /// role: String
394 /// }
395 ///
396 /// let params = QueryParamsBuilder::<UserExample>::new()
397 /// .with_filter("status", Some("active"))
398 /// .with_filter("role", Some("admin"))
399 /// .build();
400 /// ```
401 pub fn with_filter(mut self, key: impl Into<String>, value: Option<impl Into<String>>) -> Self {
402 let key = key.into();
403 let valid_fields = get_struct_field_names::<T>();
404
405 if valid_fields.contains(&key) {
406 if let Some(val) = value {
407 self.query
408 .filters
409 .insert(key, QueryFilterCondition::equal(val));
410 }
411 } else {
412 #[cfg(feature = "tracing")]
413 tracing::warn!(column = %key, "Skipping invalid filter column");
414 }
415 self
416 }
417
418 /// Adds multiple filter conditions from a HashMap (backward compatible).
419 ///
420 /// # Arguments
421 ///
422 /// * `filters` - HashMap of column names and their filter values (equality only)
423 ///
424 /// # Details
425 ///
426 /// Only adds filters for columns that exist in the model struct.
427 /// Logs a warning if tracing is enabled and a column is invalid.
428 ///
429 /// # Examples
430 ///
431 /// ```rust
432 /// use std::collections::HashMap;
433 /// use serde::{Serialize};
434 /// use sqlx_paginated::{QueryParamsBuilder};
435 ///
436 /// #[derive(Serialize, Default)]
437 /// struct UserExample {
438 /// name: String,
439 /// status: String,
440 /// role: String
441 /// }
442 ///
443 /// let mut filters = HashMap::new();
444 /// filters.insert("status", Some("active"));
445 /// filters.insert("role", Some("admin"));
446 ///
447 /// let params = QueryParamsBuilder::<UserExample>::new()
448 /// .with_filters(filters)
449 /// .build();
450 /// ```
451 pub fn with_filters(
452 mut self,
453 filters: HashMap<impl Into<String>, Option<impl Into<String>>>,
454 ) -> Self {
455 let valid_fields = get_struct_field_names::<T>();
456
457 self.query
458 .filters
459 .extend(filters.into_iter().filter_map(|(key, value)| {
460 let key = key.into();
461 if valid_fields.contains(&key) {
462 value.map(|v| (key, QueryFilterCondition::equal(v)))
463 } else {
464 #[cfg(feature = "tracing")]
465 tracing::warn!(column = %key, "Skipping invalid filter column");
466 None
467 }
468 }));
469
470 self
471 }
472
473 /// Adds multiple filter conditions with operators from a HashMap.
474 ///
475 /// # Arguments
476 ///
477 /// * `filters` - HashMap of column names and their FilterConditions
478 ///
479 /// # Details
480 ///
481 /// Only adds filters for columns that exist in the model struct.
482 /// Logs a warning if tracing is enabled and a column is invalid.
483 ///
484 /// # Examples
485 ///
486 /// ```rust
487 /// use std::collections::HashMap;
488 /// use serde::{Serialize};
489 /// use sqlx_paginated::{QueryParamsBuilder, QueryFilterCondition, QueryFilterOperator};
490 ///
491 /// #[derive(Serialize, Default)]
492 /// struct Product {
493 /// name: String,
494 /// price: f64,
495 /// stock: i32,
496 /// }
497 ///
498 /// let mut filters = HashMap::new();
499 /// filters.insert("price", QueryFilterCondition::greater_than("10.00"));
500 /// filters.insert("stock", QueryFilterCondition::less_or_equal("100"));
501 ///
502 /// let params = QueryParamsBuilder::<Product>::new()
503 /// .with_filter_conditions(filters)
504 /// .build();
505 /// ```
506 pub fn with_filter_conditions(
507 mut self,
508 filters: HashMap<impl Into<String>, QueryFilterCondition>,
509 ) -> Self {
510 let valid_fields = get_struct_field_names::<T>();
511
512 self.query
513 .filters
514 .extend(filters.into_iter().filter_map(|(key, condition)| {
515 let key = key.into();
516 if valid_fields.contains(&key) {
517 Some((key, condition))
518 } else {
519 #[cfg(feature = "tracing")]
520 tracing::warn!(column = %key, "Skipping invalid filter column");
521 None
522 }
523 }));
524
525 self
526 }
527
528 /// Builds and returns the final QueryParams.
529 ///
530 /// # Returns
531 ///
532 /// Returns the constructed `QueryParams<T>` with all the configured parameters.
533 ///
534 /// # Examples
535 ///
536 /// ```rust
537 /// use chrono::{DateTime, Utc};
538 /// use sqlx_paginated::{QueryParamsBuilder, QuerySortDirection};
539 /// use serde::{Serialize};
540 ///
541 /// #[derive(Serialize, Default)]
542 /// struct UserExample {
543 /// name: String,
544 /// status: String,
545 /// email: String,
546 /// created_at: DateTime<Utc>
547 /// }
548 ///
549 /// let params = QueryParamsBuilder::<UserExample>::new()
550 /// .with_pagination(1, 20)
551 /// .with_sort("created_at", QuerySortDirection::Descending)
552 /// .with_search("john", vec!["name", "email"])
553 /// .with_filter("status", Some("active"))
554 /// .build();
555 /// ```
556 pub fn build(self) -> QueryParams<'q, T> {
557 self.query
558 }
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564 use crate::paginated_query_as::internal::{
565 DEFAULT_SEARCH_COLUMN_NAMES, DEFAULT_SORT_COLUMN_NAME,
566 };
567 use crate::paginated_query_as::models::QuerySortDirection;
568 use chrono::{DateTime, Utc};
569 use std::collections::HashMap;
570
571 #[derive(Debug, Default, Serialize)]
572 struct TestModel {
573 name: String,
574 title: String,
575 description: String,
576 status: String,
577 category: String,
578 updated_at: DateTime<Utc>,
579 created_at: DateTime<Utc>,
580 }
581
582 #[test]
583 fn test_pagination_defaults() {
584 let params = QueryParamsBuilder::<TestModel>::new().build();
585
586 assert_eq!(
587 params.pagination.page_size, DEFAULT_MIN_PAGE_SIZE,
588 "Default page size should be {}",
589 DEFAULT_MIN_PAGE_SIZE
590 );
591 assert_eq!(
592 params.pagination.page, DEFAULT_PAGE,
593 "Default page should be {}",
594 DEFAULT_PAGE
595 );
596
597 // Test page size clamping
598 let params = QueryParamsBuilder::<TestModel>::new()
599 .with_pagination(1, DEFAULT_MAX_PAGE_SIZE + 10)
600 .build();
601
602 assert_eq!(
603 params.pagination.page_size, DEFAULT_MAX_PAGE_SIZE,
604 "Page size should be clamped to maximum {}",
605 DEFAULT_MAX_PAGE_SIZE
606 );
607
608 let params = QueryParamsBuilder::<TestModel>::new()
609 .with_pagination(1, DEFAULT_MIN_PAGE_SIZE - 5)
610 .build();
611
612 assert_eq!(
613 params.pagination.page_size, DEFAULT_MIN_PAGE_SIZE,
614 "Page size should be clamped to minimum {}",
615 DEFAULT_MIN_PAGE_SIZE
616 );
617 }
618
619 #[test]
620 fn test_default_sort_column() {
621 let params = QueryParamsBuilder::<TestModel>::new().build();
622
623 assert_eq!(
624 params.sort.sort_column, DEFAULT_SORT_COLUMN_NAME,
625 "Default sort column should be '{}'",
626 DEFAULT_SORT_COLUMN_NAME
627 );
628 }
629
630 #[test]
631 fn test_date_range_defaults() {
632 let params = QueryParamsBuilder::<TestModel>::new().build();
633
634 assert_eq!(
635 params.date_range.date_column,
636 Some(DEFAULT_DATE_RANGE_COLUMN_NAME.to_string()),
637 "Default date range column should be '{}'",
638 DEFAULT_DATE_RANGE_COLUMN_NAME
639 );
640 assert!(
641 params.date_range.date_after.is_none(),
642 "Default date_after should be None"
643 );
644 assert!(
645 params.date_range.date_before.is_none(),
646 "Default date_before should be None"
647 );
648 }
649
650 #[test]
651 fn test_search_defaults() {
652 let params = QueryParamsBuilder::<TestModel>::new().build();
653
654 // Check if default search columns are set
655 assert_eq!(
656 params.search.search_columns,
657 Some(
658 DEFAULT_SEARCH_COLUMN_NAMES
659 .iter()
660 .map(|&s| s.to_string())
661 .collect()
662 ),
663 "Default search columns should be {:?}",
664 DEFAULT_SEARCH_COLUMN_NAMES
665 );
666 assert!(
667 params.search.search.is_none(),
668 "Default search term should be None"
669 );
670 }
671
672 #[test]
673 fn test_combined_defaults() {
674 let params = QueryParamsBuilder::<TestModel>::new().build();
675
676 // Test all defaults together
677 assert_eq!(params.pagination.page, DEFAULT_PAGE);
678 assert_eq!(params.pagination.page_size, DEFAULT_MIN_PAGE_SIZE);
679 assert_eq!(params.sort.sort_column, DEFAULT_SORT_COLUMN_NAME);
680 assert_eq!(params.sort.sort_direction, QuerySortDirection::Descending);
681 assert_eq!(
682 params.date_range.date_column,
683 Some(DEFAULT_DATE_RANGE_COLUMN_NAME.to_string())
684 );
685 assert_eq!(
686 params.search.search_columns,
687 Some(
688 DEFAULT_SEARCH_COLUMN_NAMES
689 .iter()
690 .map(|&s| s.to_string())
691 .collect()
692 )
693 );
694 assert!(params.search.search.is_none());
695 assert!(params.date_range.date_after.is_none());
696 assert!(params.date_range.date_before.is_none());
697 }
698
699 #[test]
700 fn test_empty_params() {
701 let params = QueryParamsBuilder::<TestModel>::new().build();
702
703 assert_eq!(params.pagination.page, 1);
704 assert_eq!(params.pagination.page_size, 10);
705 assert_eq!(params.sort.sort_column, "created_at");
706 assert!(matches!(
707 params.sort.sort_direction,
708 QuerySortDirection::Descending
709 ));
710 }
711
712 #[test]
713 fn test_partial_params() {
714 let params = QueryParamsBuilder::<TestModel>::new()
715 .with_pagination(2, 10)
716 .with_search("test".to_string(), vec!["name".to_string()])
717 .build();
718
719 assert_eq!(params.pagination.page, 2);
720 assert_eq!(params.search.search, Some("test".to_string()));
721 assert_eq!(params.pagination.page_size, 10);
722 assert_eq!(params.sort.sort_column, "created_at");
723 assert!(matches!(
724 params.sort.sort_direction,
725 QuerySortDirection::Descending
726 ));
727 }
728
729 #[test]
730 fn test_invalid_params() {
731 // For builder pattern, invalid params would be handled at compile time
732 // But we can test the defaults
733 let params = QueryParamsBuilder::<TestModel>::new()
734 .with_pagination(0, 0) // Should be clamped to minimum values
735 .build();
736
737 assert_eq!(params.pagination.page, 1);
738 assert_eq!(params.pagination.page_size, 10);
739 }
740
741 #[test]
742 fn test_filters() {
743 let mut filters = HashMap::new();
744 filters.insert("status".to_string(), Some("active".to_string()));
745 filters.insert("category".to_string(), Some("test".to_string()));
746
747 let params = QueryParamsBuilder::<TestModel>::new()
748 .with_filters(filters)
749 .build();
750
751 assert!(params.filters.contains_key("status"));
752 let status_filter = params.filters.get("status").unwrap();
753 assert_eq!(status_filter.operator, QueryFilterOperator::Equal);
754 assert_eq!(status_filter.value, Some("active".to_string()));
755
756 assert!(params.filters.contains_key("category"));
757 let category_filter = params.filters.get("category").unwrap();
758 assert_eq!(category_filter.operator, QueryFilterOperator::Equal);
759 assert_eq!(category_filter.value, Some("test".to_string()));
760 }
761
762 #[test]
763 fn test_search_with_columns() {
764 let params = QueryParamsBuilder::<TestModel>::new()
765 .with_search(
766 "test".to_string(),
767 vec!["title".to_string(), "description".to_string()],
768 )
769 .build();
770
771 assert_eq!(params.search.search, Some("test".to_string()));
772 assert_eq!(
773 params.search.search_columns,
774 Some(vec!["title".to_string(), "description".to_string()])
775 );
776 }
777
778 #[test]
779 fn test_full_params() {
780 let params = QueryParamsBuilder::<TestModel>::new()
781 .with_pagination(2, 20)
782 .with_sort("updated_at".to_string(), QuerySortDirection::Ascending)
783 .with_search(
784 "test".to_string(),
785 vec!["title".to_string(), "description".to_string()],
786 )
787 .with_date_range(Some(Utc::now()), None, None::<String>)
788 .build();
789
790 assert_eq!(params.pagination.page, 2);
791 assert_eq!(params.pagination.page_size, 20);
792 assert_eq!(params.sort.sort_column, "updated_at");
793 assert!(matches!(
794 params.sort.sort_direction,
795 QuerySortDirection::Ascending
796 ));
797 assert_eq!(params.search.search, Some("test".to_string()));
798 assert_eq!(
799 params.search.search_columns,
800 Some(vec!["title".to_string(), "description".to_string()])
801 );
802 assert!(params.date_range.date_after.is_some());
803 assert!(params.date_range.date_before.is_none());
804 }
805
806 #[test]
807 fn test_filter_chain() {
808 let params = QueryParamsBuilder::<TestModel>::new()
809 .with_filter("status", Some("active"))
810 .with_filter("category", Some("test"))
811 .build();
812
813 let status_filter = params.filters.get("status").unwrap();
814 assert_eq!(status_filter.operator, QueryFilterOperator::Equal);
815 assert_eq!(status_filter.value, Some("active".to_string()));
816
817 let category_filter = params.filters.get("category").unwrap();
818 assert_eq!(category_filter.operator, QueryFilterOperator::Equal);
819 assert_eq!(category_filter.value, Some("test".to_string()));
820 }
821
822 #[test]
823 fn test_mixed_pagination() {
824 let params = QueryParamsBuilder::<TestModel>::new()
825 .with_pagination(2, 10)
826 .with_search("test".to_string(), vec!["title".to_string()])
827 .with_filter("status", Some("active"))
828 .build();
829
830 assert_eq!(params.pagination.page, 2);
831 assert_eq!(params.pagination.page_size, 10);
832 assert_eq!(params.search.search, Some("test".to_string()));
833
834 let status_filter = params.filters.get("status").unwrap();
835 assert_eq!(status_filter.operator, QueryFilterOperator::Equal);
836 assert_eq!(status_filter.value, Some("active".to_string()));
837 }
838
839 #[test]
840 fn test_filter_operators() {
841 let params = QueryParamsBuilder::<TestModel>::new()
842 .with_filter_operator("title", QueryFilterOperator::Like, "%test%")
843 .with_filter_operator("status", QueryFilterOperator::NotEqual, "deleted")
844 .build();
845
846 let title_filter = params.filters.get("title").unwrap();
847 assert_eq!(title_filter.operator, QueryFilterOperator::Like);
848 assert_eq!(title_filter.value, Some("%test%".to_string()));
849
850 let status_filter = params.filters.get("status").unwrap();
851 assert_eq!(status_filter.operator, QueryFilterOperator::NotEqual);
852 assert_eq!(status_filter.value, Some("deleted".to_string()));
853 }
854
855 #[test]
856 fn test_filter_null() {
857 let params = QueryParamsBuilder::<TestModel>::new()
858 .with_filter_null("description", true)
859 .build();
860
861 let filter = params.filters.get("description").unwrap();
862 assert_eq!(filter.operator, QueryFilterOperator::IsNull);
863 assert_eq!(filter.value, None);
864 }
865
866 #[test]
867 fn test_filter_in() {
868 let params = QueryParamsBuilder::<TestModel>::new()
869 .with_filter_in("status", vec!["active", "pending", "approved"])
870 .build();
871
872 let filter = params.filters.get("status").unwrap();
873 assert_eq!(filter.operator, QueryFilterOperator::In);
874 assert_eq!(filter.value, Some("active,pending,approved".to_string()));
875
876 let values = filter.split_values();
877 assert_eq!(values, vec!["active", "pending", "approved"]);
878 }
879}