actix_contrib_rest/query.rs
1//! Map query searches.
2
3use serde::Deserialize;
4use validator::Validate;
5
6fn default_page_size() -> i64 {
7 50
8}
9
10/// Struct used to deserialize with `serde` query strings
11/// from a request URL.
12///
13/// # Examples
14/// Valid URLs could be:
15/// - `/api/users?q=marian&include_total=true`
16/// - `/api/v1/sales?q=customer:john&page_size=100`
17/// - `/some-endpoint?page_size=20&sort=-name`
18///
19/// When an instance is created through serde,
20/// the `page_size` attribute is set to `50`
21/// if the stream serialized does not have the
22/// value set.
23#[derive(Debug, Clone, Deserialize, Validate, PartialEq, Eq)]
24pub struct QuerySearch {
25 pub q: Option<String>,
26 pub sort: Option<String>,
27 #[serde(default)]
28 #[validate(range(min = 0))]
29 pub offset: i64,
30 #[serde(default = "default_page_size")]
31 #[validate(range(min = 1))]
32 pub page_size: i64,
33 pub include_total: Option<bool>,
34}
35
36impl QuerySearch {
37 /// Parse sort argument "col1,col2,-col3..." into a vector of strings,
38 /// and if the column name starts with "-", it's translated to a DESC
39 /// keyword, e.g. "-name" --> "name DESC".
40 ///
41 /// ```
42 /// use actix_contrib_rest::query::QuerySearch;
43 /// let q = QuerySearch { q: None, offset: 0, page_size: 10, sort: None, include_total: None };
44 /// assert_eq!(q.parse_sort(&["a", "b"]), Vec::<String>::new());
45 /// let q = QuerySearch { q: None, offset: 0, page_size: 10, sort: Some(String::from("a,-b")), include_total: None };
46 /// assert_eq!(q.parse_sort(&["a", "b"]), &[String::from("a"), String::from("b DESC")]);
47 /// let q = QuerySearch { q: None, offset: 0, page_size: 10, sort: Some(String::from("name,-b,c")), include_total: None };
48 /// assert_eq!(q.parse_sort(&vec!["name", "c"]), &[String::from("name"), String::from("c")]);
49 /// ```
50 pub fn parse_sort(&self, allowed_fields: &[&str]) -> Vec<String> {
51 self.sort
52 .as_deref()
53 .unwrap_or("")
54 .split(',')
55 .filter(|s| allowed_fields.contains(&s.strip_prefix('-').unwrap_or(s)))
56 .map(|f| {
57 f.strip_prefix('-')
58 .map(|d| format!("{d} DESC"))
59 .unwrap_or(f.to_string())
60 })
61 .collect()
62 }
63
64 /// Parse sort argument "col1,col2,-col3..." into a compatible SQL `ORDER BY` expression,
65 /// e.g. `name,-age` --> `name, age DESC`, to be concatenated in a SQL `SELECT` query.
66 ///
67 /// ```
68 /// use actix_contrib_rest::query::QuerySearch;
69 /// let q = QuerySearch { q: None, offset: 0, page_size: 10, sort: None, include_total: None };
70 /// assert_eq!(q.sort_as_order_by_args(&["a", "b"], "a"), "a");
71 /// let q = QuerySearch { q: None, offset: 0, page_size: 10, sort: Some(String::from("a,-b")), include_total: None };
72 /// assert_eq!(q.sort_as_order_by_args(&["a", "b"], "a"), "a, b DESC");
73 /// let q = QuerySearch { q: None, offset: 0, page_size: 10, sort: Some(String::from("name,-b,c")), include_total: None };
74 /// assert_eq!(q.sort_as_order_by_args(&["a", "h"], "c"), "c");
75 /// ```
76 pub fn sort_as_order_by_args(&self, allowed_fields: &[&str], default: &str) -> String {
77 let sorting = self.parse_sort(allowed_fields);
78 match sorting.len() {
79 0 => String::from(default),
80 _ => sorting.join(", "),
81 }
82 }
83}
84
85
86/// Struct used to deserialize with `serde` query strings
87/// from a request URL with the `force` argument, that
88/// can be either true or false, or not be set at all.
89#[derive(Debug, Clone, Deserialize, Validate)]
90pub struct Force {
91 pub force: Option<bool>,
92}