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, PartialEq, Eq)]
143#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
144#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
145#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
146#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
147#[cfg_attr(feature = "proptest", derive(proptest_derive::Arbitrary))]
148pub struct FilterEntry {
149 pub field: String,
151 pub operator: String,
153 pub value: String,
155}
156
157#[cfg(any(feature = "std", feature = "alloc"))]
158impl FilterEntry {
159 #[must_use]
172 pub fn new(
173 field: impl Into<String>,
174 operator: impl Into<String>,
175 value: impl Into<String>,
176 ) -> Self {
177 Self {
178 field: field.into(),
179 operator: operator.into(),
180 value: value.into(),
181 }
182 }
183}
184
185#[cfg(any(feature = "std", feature = "alloc"))]
194#[derive(Debug, Clone, PartialEq, Eq, Default)]
195#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
196#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema, utoipa::IntoParams))]
197#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
198#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
199#[cfg_attr(feature = "proptest", derive(proptest_derive::Arbitrary))]
200pub struct FilterParams {
201 #[cfg_attr(feature = "serde", serde(default))]
203 pub filters: Vec<FilterEntry>,
204}
205
206#[cfg(any(feature = "std", feature = "alloc"))]
207impl FilterParams {
208 #[must_use]
220 pub fn new(filters: impl IntoIterator<Item = FilterEntry>) -> Self {
221 Self {
222 filters: filters.into_iter().collect(),
223 }
224 }
225
226 #[must_use]
237 pub fn is_empty(&self) -> bool {
238 self.filters.is_empty()
239 }
240}
241
242#[cfg(any(feature = "std", feature = "alloc"))]
255#[derive(Debug, Clone, PartialEq, Eq)]
256#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
257#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema, utoipa::IntoParams))]
258#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
259#[cfg_attr(feature = "validator", derive(Validate))]
260#[cfg_attr(feature = "proptest", derive(proptest_derive::Arbitrary))]
261pub struct SearchParams {
262 #[cfg_attr(
264 feature = "validator",
265 validate(length(
266 min = 1,
267 max = 500,
268 message = "query must be between 1 and 500 characters"
269 ))
270 )]
271 #[cfg_attr(feature = "proptest", proptest(strategy = "search_query_strategy()"))]
272 pub query: String,
273 #[cfg_attr(
275 feature = "serde",
276 serde(default, skip_serializing_if = "Vec::is_empty")
277 )]
278 pub fields: Vec<String>,
279}
280
281#[cfg(any(feature = "std", feature = "alloc"))]
282impl SearchParams {
283 #[must_use]
295 pub fn new(query: impl Into<String>) -> Self {
296 Self {
297 query: query.into(),
298 fields: Vec::new(),
299 }
300 }
301
302 pub fn try_new(query: impl Into<String>) -> Result<Self, crate::error::ValidationError> {
316 let query = query.into();
317 if query.is_empty() || query.len() > 500 {
318 return Err(crate::error::ValidationError {
319 field: "/query".into(),
320 message: "must be between 1 and 500 characters".into(),
321 rule: Some("length".into()),
322 });
323 }
324 Ok(Self {
325 query,
326 fields: Vec::new(),
327 })
328 }
329
330 pub fn try_with_fields(
343 query: impl Into<String>,
344 fields: impl IntoIterator<Item = impl Into<String>>,
345 ) -> Result<Self, crate::error::ValidationError> {
346 let mut s = Self::try_new(query)?;
347 s.fields = fields.into_iter().map(Into::into).collect();
348 Ok(s)
349 }
350
351 #[must_use]
363 pub fn with_fields(
364 query: impl Into<String>,
365 fields: impl IntoIterator<Item = impl Into<String>>,
366 ) -> Self {
367 Self {
368 query: query.into(),
369 fields: fields.into_iter().map(Into::into).collect(),
370 }
371 }
372}
373
374#[cfg(feature = "axum")]
379#[allow(clippy::result_large_err)]
380mod axum_extractors {
381 use super::SortParams;
382 use crate::error::ApiError;
383 use axum::extract::{FromRequestParts, Query};
384 use axum::http::request::Parts;
385
386 impl<S: Send + Sync> FromRequestParts<S> for SortParams {
387 type Rejection = ApiError;
388
389 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
390 let Query(params) = Query::<Self>::from_request_parts(parts, state)
391 .await
392 .map_err(|e| ApiError::bad_request(e.to_string()))?;
393 Ok(params)
394 }
395 }
396}
397
398#[cfg(all(feature = "proptest", any(feature = "std", feature = "alloc")))]
403fn search_query_strategy() -> impl proptest::strategy::Strategy<Value = String> {
404 proptest::string::string_regex("[a-zA-Z0-9 ]{1,500}").expect("valid regex")
405}
406
407#[cfg(all(feature = "arbitrary", any(feature = "std", feature = "alloc")))]
412impl<'a> arbitrary::Arbitrary<'a> for SearchParams {
413 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
414 let len = u.int_in_range(1usize..=500)?;
417 let query: String = (0..len)
418 .map(|_| -> arbitrary::Result<char> {
419 let byte = u.int_in_range(32u8..=126)?;
420 Ok(char::from(byte))
421 })
422 .collect::<arbitrary::Result<_>>()?;
423 let fields = <Vec<String> as arbitrary::Arbitrary>::arbitrary(u)?;
424 Ok(Self { query, fields })
425 }
426}
427
428#[cfg(test)]
433mod tests {
434 use super::*;
435
436 #[test]
439 fn sort_direction_default_is_asc() {
440 assert_eq!(SortDirection::default(), SortDirection::Asc);
441 }
442
443 #[cfg(feature = "serde")]
444 #[test]
445 fn sort_direction_serde_lowercase() {
446 let asc = serde_json::to_value(SortDirection::Asc).unwrap();
447 assert_eq!(asc, serde_json::json!("asc"));
448 let desc = serde_json::to_value(SortDirection::Desc).unwrap();
449 assert_eq!(desc, serde_json::json!("desc"));
450
451 let back: SortDirection = serde_json::from_value(asc).unwrap();
452 assert_eq!(back, SortDirection::Asc);
453 }
454
455 #[test]
458 fn sort_params_asc_helper() {
459 let p = SortParams::asc("created_at");
460 assert_eq!(p.sort_by, "created_at");
461 assert_eq!(p.direction, SortDirection::Asc);
462 }
463
464 #[test]
465 fn sort_params_desc_helper() {
466 let p = SortParams::desc("name");
467 assert_eq!(p.sort_by, "name");
468 assert_eq!(p.direction, SortDirection::Desc);
469 }
470
471 #[cfg(feature = "serde")]
472 #[test]
473 fn sort_params_serde_round_trip() {
474 let p = SortParams::desc("created_at");
475 let json = serde_json::to_value(&p).unwrap();
476 assert_eq!(json["sort_by"], "created_at");
477 assert_eq!(json["direction"], "desc");
478 let back: SortParams = serde_json::from_value(json).unwrap();
479 assert_eq!(back, p);
480 }
481
482 #[cfg(feature = "serde")]
483 #[test]
484 fn sort_params_serde_default_direction() {
485 let json = serde_json::json!({"sort_by": "name"});
486 let p: SortParams = serde_json::from_value(json).unwrap();
487 assert_eq!(p.direction, SortDirection::Asc);
488 }
489
490 #[test]
493 fn filter_params_default_is_empty() {
494 let f = FilterParams::default();
495 assert!(f.is_empty());
496 }
497
498 #[test]
499 fn filter_params_new() {
500 let f = FilterParams::new([FilterEntry::new("status", "eq", "active")]);
501 assert!(!f.is_empty());
502 assert_eq!(f.filters.len(), 1);
503 assert_eq!(f.filters[0].field, "status");
504 assert_eq!(f.filters[0].operator, "eq");
505 assert_eq!(f.filters[0].value, "active");
506 }
507
508 #[cfg(feature = "serde")]
509 #[test]
510 fn filter_params_serde_round_trip() {
511 let f = FilterParams::new([FilterEntry::new("age", "gt", "18")]);
512 let json = serde_json::to_value(&f).unwrap();
513 let back: FilterParams = serde_json::from_value(json).unwrap();
514 assert_eq!(back, f);
515 }
516
517 #[cfg(feature = "serde")]
518 #[test]
519 fn filter_params_serde_empty_filters_default() {
520 let json = serde_json::json!({});
521 let f: FilterParams = serde_json::from_value(json).unwrap();
522 assert!(f.is_empty());
523 }
524
525 #[test]
528 fn search_params_new() {
529 let s = SearchParams::new("annual report");
530 assert_eq!(s.query, "annual report");
531 assert!(s.fields.is_empty());
532 }
533
534 #[test]
535 fn search_params_with_fields() {
536 let s = SearchParams::with_fields("report", ["title", "description"]);
537 assert_eq!(s.query, "report");
538 assert_eq!(s.fields, vec!["title", "description"]);
539 }
540
541 #[cfg(feature = "serde")]
542 #[test]
543 fn search_params_serde_round_trip() {
544 let s = SearchParams::with_fields("hello", ["name"]);
545 let json = serde_json::to_value(&s).unwrap();
546 assert_eq!(json["query"], "hello");
547 assert_eq!(json["fields"], serde_json::json!(["name"]));
548 let back: SearchParams = serde_json::from_value(json).unwrap();
549 assert_eq!(back, s);
550 }
551
552 #[cfg(feature = "serde")]
553 #[test]
554 fn search_params_serde_omits_empty_fields() {
555 let s = SearchParams::new("test");
556 let json = serde_json::to_value(&s).unwrap();
557 assert!(json.get("fields").is_none());
558 }
559
560 #[cfg(feature = "validator")]
561 #[test]
562 fn search_params_validate_empty_query_fails() {
563 use validator::Validate;
564 let s = SearchParams::new("");
565 assert!(s.validate().is_err());
566 }
567
568 #[cfg(feature = "validator")]
569 #[test]
570 fn search_params_validate_too_long_fails() {
571 use validator::Validate;
572 let s = SearchParams::new("a".repeat(501));
573 assert!(s.validate().is_err());
574 }
575
576 #[cfg(feature = "validator")]
577 #[test]
578 fn search_params_validate_boundary_max() {
579 use validator::Validate;
580 let s = SearchParams::new("a".repeat(500));
581 assert!(s.validate().is_ok());
582 }
583
584 #[test]
589 fn search_params_try_new_valid() {
590 let s = SearchParams::try_new("report").unwrap();
591 assert_eq!(s.query, "report");
592 assert!(s.fields.is_empty());
593 }
594
595 #[test]
596 fn search_params_try_new_boundary_min() {
597 assert!(SearchParams::try_new("a").is_ok());
598 }
599
600 #[test]
601 fn search_params_try_new_boundary_max() {
602 assert!(SearchParams::try_new("a".repeat(500)).is_ok());
603 }
604
605 #[test]
606 fn search_params_try_new_empty_fails() {
607 let err = SearchParams::try_new("").unwrap_err();
608 assert_eq!(err.field, "/query");
609 assert_eq!(err.rule.as_deref(), Some("length"));
610 }
611
612 #[test]
613 fn search_params_try_new_too_long_fails() {
614 assert!(SearchParams::try_new("a".repeat(501)).is_err());
615 }
616
617 #[test]
618 fn search_params_try_with_fields_valid() {
619 let s = SearchParams::try_with_fields("report", ["title", "body"]).unwrap();
620 assert_eq!(s.query, "report");
621 assert_eq!(s.fields, vec!["title", "body"]);
622 }
623
624 #[test]
625 fn search_params_try_with_fields_empty_query_fails() {
626 assert!(SearchParams::try_with_fields("", ["title"]).is_err());
627 }
628
629 #[cfg(feature = "axum")]
630 mod axum_extractor_tests {
631 use super::super::{SortDirection, SortParams};
632 use axum::extract::FromRequestParts;
633 use axum::http::Request;
634
635 async fn extract(q: &str) -> Result<SortParams, u16> {
636 let req = Request::builder().uri(format!("/?{q}")).body(()).unwrap();
637 let (mut parts, ()) = req.into_parts();
638 SortParams::from_request_parts(&mut parts, &())
639 .await
640 .map_err(|e| e.status)
641 }
642
643 #[tokio::test]
644 async fn sort_default_direction() {
645 let p = extract("sort_by=name").await.unwrap();
646 assert_eq!(p.sort_by, "name");
647 assert_eq!(p.direction, SortDirection::Asc);
648 }
649
650 #[tokio::test]
651 async fn sort_custom_direction() {
652 let p = extract("sort_by=created_at&direction=desc").await.unwrap();
653 assert_eq!(p.direction, SortDirection::Desc);
654 }
655
656 #[tokio::test]
657 async fn sort_missing_sort_by_rejected() {
658 assert_eq!(extract("").await.unwrap_err(), 400);
659 }
660 }
661
662 #[cfg(feature = "validator")]
663 #[test]
664 fn search_params_validate_ok() {
665 use validator::Validate;
666 let s = SearchParams::new("valid query");
667 assert!(s.validate().is_ok());
668 }
669}