Skip to main content

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}