Skip to main content

shaperail_runtime/db/
sort.rs

1/// Sort direction for a field.
2#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3pub enum SortDirection {
4    Asc,
5    Desc,
6}
7
8impl std::fmt::Display for SortDirection {
9    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
10        match self {
11            Self::Asc => write!(f, "ASC"),
12            Self::Desc => write!(f, "DESC"),
13        }
14    }
15}
16
17/// A single field + direction sort instruction.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct SortField {
20    pub field: String,
21    pub direction: SortDirection,
22}
23
24/// Parsed sort parameters from `?sort=-created_at,name`.
25///
26/// A leading `-` means descending; no prefix means ascending.
27#[derive(Debug, Clone, Default)]
28pub struct SortParam {
29    pub fields: Vec<SortField>,
30}
31
32impl SortParam {
33    /// Parses a sort query string value like `-created_at,name`.
34    ///
35    /// Only includes fields present in `allowed_fields`.
36    pub fn parse(raw: &str, allowed_fields: &[String]) -> Self {
37        let mut fields = Vec::new();
38        for part in raw.split(',') {
39            let part = part.trim();
40            if part.is_empty() {
41                continue;
42            }
43            let (direction, field_name) = if let Some(name) = part.strip_prefix('-') {
44                (SortDirection::Desc, name)
45            } else {
46                (SortDirection::Asc, part)
47            };
48            if allowed_fields.iter().any(|f| f == field_name) {
49                fields.push(SortField {
50                    field: field_name.to_string(),
51                    direction,
52                });
53            }
54        }
55        SortParam { fields }
56    }
57
58    /// Returns true if there are no sort fields.
59    pub fn is_empty(&self) -> bool {
60        self.fields.is_empty()
61    }
62
63    /// Appends an ORDER BY clause to the given SQL string.
64    pub fn apply_to_sql(&self, sql: &mut String) {
65        if self.fields.is_empty() {
66            return;
67        }
68        sql.push_str(" ORDER BY ");
69        for (i, sf) in self.fields.iter().enumerate() {
70            if i > 0 {
71                sql.push_str(", ");
72            }
73            sql.push_str(&format!("\"{}\" {}", sf.field, sf.direction));
74        }
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn parse_sort_params() {
84        let allowed = vec![
85            "created_at".to_string(),
86            "name".to_string(),
87            "email".to_string(),
88        ];
89        let sp = SortParam::parse("-created_at,name", &allowed);
90
91        assert_eq!(sp.fields.len(), 2);
92        assert_eq!(sp.fields[0].field, "created_at");
93        assert_eq!(sp.fields[0].direction, SortDirection::Desc);
94        assert_eq!(sp.fields[1].field, "name");
95        assert_eq!(sp.fields[1].direction, SortDirection::Asc);
96    }
97
98    #[test]
99    fn sort_disallowed_fields_ignored() {
100        let allowed = vec!["name".to_string()];
101        let sp = SortParam::parse("-secret,name", &allowed);
102
103        assert_eq!(sp.fields.len(), 1);
104        assert_eq!(sp.fields[0].field, "name");
105    }
106
107    #[test]
108    fn sort_apply_to_sql() {
109        let sp = SortParam {
110            fields: vec![
111                SortField {
112                    field: "created_at".to_string(),
113                    direction: SortDirection::Desc,
114                },
115                SortField {
116                    field: "name".to_string(),
117                    direction: SortDirection::Asc,
118                },
119            ],
120        };
121
122        let mut sql = "SELECT * FROM users".to_string();
123        sp.apply_to_sql(&mut sql);
124
125        assert_eq!(
126            sql,
127            "SELECT * FROM users ORDER BY \"created_at\" DESC, \"name\" ASC"
128        );
129    }
130
131    #[test]
132    fn empty_sort_no_clause() {
133        let sp = SortParam::default();
134        assert!(sp.is_empty());
135
136        let mut sql = "SELECT * FROM users".to_string();
137        sp.apply_to_sql(&mut sql);
138        assert_eq!(sql, "SELECT * FROM users");
139    }
140
141    #[test]
142    fn sort_direction_display() {
143        assert_eq!(SortDirection::Asc.to_string(), "ASC");
144        assert_eq!(SortDirection::Desc.to_string(), "DESC");
145    }
146}