shaperail_runtime/db/
sort.rs1#[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#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct SortField {
20 pub field: String,
21 pub direction: SortDirection,
22}
23
24#[derive(Debug, Clone, Default)]
28pub struct SortParam {
29 pub fields: Vec<SortField>,
30}
31
32impl SortParam {
33 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 pub fn is_empty(&self) -> bool {
60 self.fields.is_empty()
61 }
62
63 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}