sqlx_paginated/paginated_query_as/builders/
query_params_builder.rs1use 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 pub fn new() -> Self {
44 Self {
45 query: QueryParams::default(),
46 }
47 }
48
49 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 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 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 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 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 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 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 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 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 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 let params = QueryParamsBuilder::<TestModel>::new()
495 .with_pagination(0, 0) .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}