1#[cfg(all(not(feature = "std"), feature = "alloc"))]
14use alloc::{string::String, vec::Vec};
15#[cfg(feature = "serde")]
16use serde::{Deserialize, Serialize};
17#[cfg(all(feature = "validator", any(feature = "std", feature = "alloc")))]
18use validator::Validate;
19
20#[derive(Debug, Clone, PartialEq, Eq, Default)]
34#[cfg_attr(
35 feature = "serde",
36 derive(Serialize, Deserialize),
37 serde(rename_all = "lowercase")
38)]
39#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
40#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
41#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
42#[cfg_attr(feature = "proptest", derive(proptest_derive::Arbitrary))]
43pub enum SortDirection {
44 #[default]
46 Asc,
47 Desc,
49}
50
51#[cfg(all(feature = "serde", any(feature = "std", feature = "alloc")))]
56fn default_sort_direction() -> SortDirection {
57 SortDirection::Asc
58}
59
60#[cfg(any(feature = "std", feature = "alloc"))]
66#[derive(Debug, Clone, PartialEq, Eq)]
67#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
68#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema, utoipa::IntoParams))]
69#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
70#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
71#[cfg_attr(feature = "proptest", derive(proptest_derive::Arbitrary))]
72pub struct SortParams {
73 pub sort_by: String,
75 #[cfg_attr(feature = "serde", serde(default = "default_sort_direction"))]
77 pub direction: SortDirection,
78}
79
80#[cfg(any(feature = "std", feature = "alloc"))]
81impl SortParams {
82 #[must_use]
94 pub fn new(sort_by: impl Into<String>, direction: SortDirection) -> Self {
95 Self {
96 sort_by: sort_by.into(),
97 direction,
98 }
99 }
100
101 #[must_use]
112 pub fn asc(sort_by: impl Into<String>) -> Self {
113 Self::new(sort_by, SortDirection::Asc)
114 }
115
116 #[must_use]
127 pub fn desc(sort_by: impl Into<String>) -> Self {
128 Self::new(sort_by, SortDirection::Desc)
129 }
130}
131
132#[cfg(any(feature = "std", feature = "alloc"))]
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
143#[cfg_attr(
144 feature = "serde",
145 derive(Serialize, Deserialize),
146 serde(rename_all = "snake_case")
147)]
148#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
149#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
150#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
151#[cfg_attr(feature = "proptest", derive(proptest_derive::Arbitrary))]
152pub enum FilterOp {
153 Eq,
155 Neq,
157 Gt,
159 Gte,
161 Lt,
163 Lte,
165 In,
167 NotIn,
169 Contains,
171 StartsWith,
173 EndsWith,
175 Exists,
177 NotExists,
179}
180
181#[cfg(any(feature = "std", feature = "alloc"))]
187#[derive(Debug, Clone, PartialEq, Eq)]
188#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
189#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
190#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
191#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
192#[cfg_attr(feature = "proptest", derive(proptest_derive::Arbitrary))]
193pub struct FilterEntry {
194 pub field: String,
196 pub op: FilterOp,
199 pub value: String,
202}
203
204#[cfg(any(feature = "std", feature = "alloc"))]
205impl FilterEntry {
206 #[must_use]
219 pub fn new(field: impl Into<String>, op: FilterOp, value: impl Into<String>) -> Self {
220 Self {
221 field: field.into(),
222 op,
223 value: value.into(),
224 }
225 }
226}
227
228#[cfg(any(feature = "std", feature = "alloc"))]
237#[derive(Debug, Clone, PartialEq, Eq, Default)]
238#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
239#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema, utoipa::IntoParams))]
240#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
241#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
242#[cfg_attr(feature = "proptest", derive(proptest_derive::Arbitrary))]
243pub struct FilterParams {
244 #[cfg_attr(feature = "serde", serde(default))]
246 pub filters: Vec<FilterEntry>,
247}
248
249#[cfg(any(feature = "std", feature = "alloc"))]
250impl FilterParams {
251 #[must_use]
263 pub fn new(filters: impl IntoIterator<Item = FilterEntry>) -> Self {
264 Self {
265 filters: filters.into_iter().collect(),
266 }
267 }
268
269 #[must_use]
280 pub fn is_empty(&self) -> bool {
281 self.filters.is_empty()
282 }
283}
284
285#[cfg(any(feature = "std", feature = "alloc"))]
298#[derive(Debug, Clone, PartialEq, Eq)]
299#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
300#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema, utoipa::IntoParams))]
301#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
302#[cfg_attr(feature = "validator", derive(Validate))]
303#[cfg_attr(feature = "proptest", derive(proptest_derive::Arbitrary))]
304pub struct SearchParams {
305 #[cfg_attr(
307 feature = "validator",
308 validate(length(
309 min = 1,
310 max = 500,
311 message = "query must be between 1 and 500 characters"
312 ))
313 )]
314 #[cfg_attr(feature = "proptest", proptest(strategy = "search_query_strategy()"))]
315 pub query: String,
316 #[cfg_attr(
318 feature = "serde",
319 serde(default, skip_serializing_if = "Vec::is_empty")
320 )]
321 pub fields: Vec<String>,
322}
323
324#[cfg(any(feature = "std", feature = "alloc"))]
325impl SearchParams {
326 #[must_use]
338 pub fn new(query: impl Into<String>) -> Self {
339 Self {
340 query: query.into(),
341 fields: Vec::new(),
342 }
343 }
344
345 pub fn try_new(query: impl Into<String>) -> Result<Self, crate::error::ValidationError> {
359 let query = query.into();
360 if query.is_empty() || query.len() > 500 {
361 return Err(crate::error::ValidationError {
362 field: "/query".into(),
363 message: "must be between 1 and 500 characters".into(),
364 rule: Some("length".into()),
365 });
366 }
367 Ok(Self {
368 query,
369 fields: Vec::new(),
370 })
371 }
372
373 pub fn try_with_fields(
386 query: impl Into<String>,
387 fields: impl IntoIterator<Item = impl Into<String>>,
388 ) -> Result<Self, crate::error::ValidationError> {
389 let mut s = Self::try_new(query)?;
390 s.fields = fields.into_iter().map(Into::into).collect();
391 Ok(s)
392 }
393
394 #[must_use]
406 pub fn with_fields(
407 query: impl Into<String>,
408 fields: impl IntoIterator<Item = impl Into<String>>,
409 ) -> Self {
410 Self {
411 query: query.into(),
412 fields: fields.into_iter().map(Into::into).collect(),
413 }
414 }
415}
416
417#[cfg(feature = "axum")]
422#[allow(clippy::result_large_err)]
423mod axum_extractors {
424 use super::SortParams;
425 use crate::error::ApiError;
426 use axum::extract::{FromRequestParts, Query};
427 use axum::http::request::Parts;
428
429 impl<S: Send + Sync> FromRequestParts<S> for SortParams {
430 type Rejection = ApiError;
431
432 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
433 let Query(params) = Query::<Self>::from_request_parts(parts, state)
434 .await
435 .map_err(|e| ApiError::bad_request(e.to_string()))?;
436 Ok(params)
437 }
438 }
439}
440
441#[cfg(all(feature = "proptest", any(feature = "std", feature = "alloc")))]
446fn search_query_strategy() -> impl proptest::strategy::Strategy<Value = String> {
447 proptest::string::string_regex("[a-zA-Z0-9 ]{1,500}").expect("valid regex")
448}
449
450#[cfg(all(feature = "arbitrary", any(feature = "std", feature = "alloc")))]
455impl<'a> arbitrary::Arbitrary<'a> for SearchParams {
456 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
457 let len = u.int_in_range(1usize..=500)?;
460 let query: String = (0..len)
461 .map(|_| -> arbitrary::Result<char> {
462 let byte = u.int_in_range(32u8..=126)?;
463 Ok(char::from(byte))
464 })
465 .collect::<arbitrary::Result<_>>()?;
466 let fields = <Vec<String> as arbitrary::Arbitrary>::arbitrary(u)?;
467 Ok(Self { query, fields })
468 }
469}
470
471#[cfg(test)]
476mod tests {
477 use super::*;
478
479 #[test]
482 fn sort_direction_default_is_asc() {
483 assert_eq!(SortDirection::default(), SortDirection::Asc);
484 }
485
486 #[cfg(feature = "serde")]
487 #[test]
488 fn sort_direction_serde_lowercase() {
489 let asc = serde_json::to_value(SortDirection::Asc).unwrap();
490 assert_eq!(asc, serde_json::json!("asc"));
491 let desc = serde_json::to_value(SortDirection::Desc).unwrap();
492 assert_eq!(desc, serde_json::json!("desc"));
493
494 let back: SortDirection = serde_json::from_value(asc).unwrap();
495 assert_eq!(back, SortDirection::Asc);
496 }
497
498 #[test]
501 fn sort_params_asc_helper() {
502 let p = SortParams::asc("created_at");
503 assert_eq!(p.sort_by, "created_at");
504 assert_eq!(p.direction, SortDirection::Asc);
505 }
506
507 #[test]
508 fn sort_params_desc_helper() {
509 let p = SortParams::desc("name");
510 assert_eq!(p.sort_by, "name");
511 assert_eq!(p.direction, SortDirection::Desc);
512 }
513
514 #[cfg(feature = "serde")]
515 #[test]
516 fn sort_params_serde_round_trip() {
517 let p = SortParams::desc("created_at");
518 let json = serde_json::to_value(&p).unwrap();
519 assert_eq!(json["sort_by"], "created_at");
520 assert_eq!(json["direction"], "desc");
521 let back: SortParams = serde_json::from_value(json).unwrap();
522 assert_eq!(back, p);
523 }
524
525 #[cfg(feature = "serde")]
526 #[test]
527 fn sort_params_serde_default_direction() {
528 let json = serde_json::json!({"sort_by": "name"});
529 let p: SortParams = serde_json::from_value(json).unwrap();
530 assert_eq!(p.direction, SortDirection::Asc);
531 }
532
533 #[test]
536 fn filter_params_default_is_empty() {
537 let f = FilterParams::default();
538 assert!(f.is_empty());
539 }
540
541 #[test]
542 fn filter_params_new() {
543 let f = FilterParams::new([FilterEntry::new("status", FilterOp::Eq, "active")]);
544 assert!(!f.is_empty());
545 assert_eq!(f.filters.len(), 1);
546 assert_eq!(f.filters[0].field, "status");
547 assert_eq!(f.filters[0].op, FilterOp::Eq);
548 assert_eq!(f.filters[0].value, "active");
549 }
550
551 #[cfg(feature = "serde")]
552 #[test]
553 fn filter_params_serde_round_trip() {
554 let f = FilterParams::new([FilterEntry::new("age", FilterOp::Gt, "18")]);
555 let json = serde_json::to_value(&f).unwrap();
556 let back: FilterParams = serde_json::from_value(json).unwrap();
557 assert_eq!(back, f);
558 }
559
560 #[cfg(feature = "serde")]
561 #[test]
562 fn filter_op_serde_snake_case() {
563 assert_eq!(
564 serde_json::to_value(FilterOp::Eq).unwrap(),
565 serde_json::json!("eq")
566 );
567 assert_eq!(
568 serde_json::to_value(FilterOp::Neq).unwrap(),
569 serde_json::json!("neq")
570 );
571 assert_eq!(
572 serde_json::to_value(FilterOp::StartsWith).unwrap(),
573 serde_json::json!("starts_with")
574 );
575 assert_eq!(
576 serde_json::to_value(FilterOp::NotExists).unwrap(),
577 serde_json::json!("not_exists")
578 );
579 let back: FilterOp = serde_json::from_value(serde_json::json!("gte")).unwrap();
580 assert_eq!(back, FilterOp::Gte);
581 }
582
583 #[cfg(feature = "serde")]
584 #[test]
585 fn filter_params_serde_empty_filters_default() {
586 let json = serde_json::json!({});
587 let f: FilterParams = serde_json::from_value(json).unwrap();
588 assert!(f.is_empty());
589 }
590
591 #[test]
594 fn search_params_new() {
595 let s = SearchParams::new("annual report");
596 assert_eq!(s.query, "annual report");
597 assert!(s.fields.is_empty());
598 }
599
600 #[test]
601 fn search_params_with_fields() {
602 let s = SearchParams::with_fields("report", ["title", "description"]);
603 assert_eq!(s.query, "report");
604 assert_eq!(s.fields, vec!["title", "description"]);
605 }
606
607 #[cfg(feature = "serde")]
608 #[test]
609 fn search_params_serde_round_trip() {
610 let s = SearchParams::with_fields("hello", ["name"]);
611 let json = serde_json::to_value(&s).unwrap();
612 assert_eq!(json["query"], "hello");
613 assert_eq!(json["fields"], serde_json::json!(["name"]));
614 let back: SearchParams = serde_json::from_value(json).unwrap();
615 assert_eq!(back, s);
616 }
617
618 #[cfg(feature = "serde")]
619 #[test]
620 fn search_params_serde_omits_empty_fields() {
621 let s = SearchParams::new("test");
622 let json = serde_json::to_value(&s).unwrap();
623 assert!(json.get("fields").is_none());
624 }
625
626 #[cfg(feature = "validator")]
627 #[test]
628 fn search_params_validate_empty_query_fails() {
629 use validator::Validate;
630 let s = SearchParams::new("");
631 assert!(s.validate().is_err());
632 }
633
634 #[cfg(feature = "validator")]
635 #[test]
636 fn search_params_validate_too_long_fails() {
637 use validator::Validate;
638 let s = SearchParams::new("a".repeat(501));
639 assert!(s.validate().is_err());
640 }
641
642 #[cfg(feature = "validator")]
643 #[test]
644 fn search_params_validate_boundary_max() {
645 use validator::Validate;
646 let s = SearchParams::new("a".repeat(500));
647 assert!(s.validate().is_ok());
648 }
649
650 #[test]
655 fn search_params_try_new_valid() {
656 let s = SearchParams::try_new("report").unwrap();
657 assert_eq!(s.query, "report");
658 assert!(s.fields.is_empty());
659 }
660
661 #[test]
662 fn search_params_try_new_boundary_min() {
663 assert!(SearchParams::try_new("a").is_ok());
664 }
665
666 #[test]
667 fn search_params_try_new_boundary_max() {
668 assert!(SearchParams::try_new("a".repeat(500)).is_ok());
669 }
670
671 #[test]
672 fn search_params_try_new_empty_fails() {
673 let err = SearchParams::try_new("").unwrap_err();
674 assert_eq!(err.field, "/query");
675 assert_eq!(err.rule.as_deref(), Some("length"));
676 }
677
678 #[test]
679 fn search_params_try_new_too_long_fails() {
680 assert!(SearchParams::try_new("a".repeat(501)).is_err());
681 }
682
683 #[test]
684 fn search_params_try_with_fields_valid() {
685 let s = SearchParams::try_with_fields("report", ["title", "body"]).unwrap();
686 assert_eq!(s.query, "report");
687 assert_eq!(s.fields, vec!["title", "body"]);
688 }
689
690 #[test]
691 fn search_params_try_with_fields_empty_query_fails() {
692 assert!(SearchParams::try_with_fields("", ["title"]).is_err());
693 }
694
695 #[cfg(feature = "axum")]
696 mod axum_extractor_tests {
697 use super::super::{SortDirection, SortParams};
698 use axum::extract::FromRequestParts;
699 use axum::http::Request;
700
701 async fn extract(q: &str) -> Result<SortParams, u16> {
702 let req = Request::builder().uri(format!("/?{q}")).body(()).unwrap();
703 let (mut parts, ()) = req.into_parts();
704 SortParams::from_request_parts(&mut parts, &())
705 .await
706 .map_err(|e| e.status)
707 }
708
709 #[tokio::test]
710 async fn sort_default_direction() {
711 let p = extract("sort_by=name").await.unwrap();
712 assert_eq!(p.sort_by, "name");
713 assert_eq!(p.direction, SortDirection::Asc);
714 }
715
716 #[tokio::test]
717 async fn sort_custom_direction() {
718 let p = extract("sort_by=created_at&direction=desc").await.unwrap();
719 assert_eq!(p.direction, SortDirection::Desc);
720 }
721
722 #[tokio::test]
723 async fn sort_missing_sort_by_rejected() {
724 assert_eq!(extract("").await.unwrap_err(), 400);
725 }
726 }
727
728 #[cfg(feature = "validator")]
729 #[test]
730 fn search_params_validate_ok() {
731 use validator::Validate;
732 let s = SearchParams::new("valid query");
733 assert!(s.validate().is_ok());
734 }
735}