shaperail_runtime/db/
filter.rs1use std::collections::HashMap;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct FilterParam {
6 pub field: String,
8 pub value: String,
10}
11
12#[derive(Debug, Clone, Default)]
14pub struct FilterSet {
15 pub filters: Vec<FilterParam>,
16}
17
18impl FilterSet {
19 pub fn from_query_params(params: &HashMap<String, String>, allowed_fields: &[String]) -> Self {
24 let mut filters = Vec::new();
25 for (key, value) in params {
26 if let Some(field) = key
27 .strip_prefix("filter[")
28 .and_then(|s| s.strip_suffix(']'))
29 {
30 if allowed_fields.iter().any(|f| f == field) {
31 filters.push(FilterParam {
32 field: field.to_string(),
33 value: value.clone(),
34 });
35 }
36 }
37 }
38 FilterSet { filters }
39 }
40
41 pub fn add(&mut self, field: String, value: String) {
43 self.filters.push(FilterParam { field, value });
44 }
45
46 pub fn is_empty(&self) -> bool {
48 self.filters.is_empty()
49 }
50
51 pub fn apply_to_sql(&self, sql: &mut String, has_where: bool, param_offset: usize) -> usize {
56 let mut offset = param_offset;
57 for (i, filter) in self.filters.iter().enumerate() {
58 if i == 0 && !has_where {
59 sql.push_str(" WHERE ");
60 } else {
61 sql.push_str(" AND ");
62 }
63 sql.push_str(&format!("\"{}\" = ${}", filter.field, offset));
64 offset += 1;
65 }
66 offset
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn parse_filter_params() {
76 let mut params = HashMap::new();
77 params.insert("filter[role]".to_string(), "admin".to_string());
78 params.insert("filter[org_id]".to_string(), "abc-123".to_string());
79 params.insert("other_param".to_string(), "ignored".to_string());
80 params.insert("filter[secret]".to_string(), "blocked".to_string());
81
82 let allowed = vec!["role".to_string(), "org_id".to_string()];
83 let fs = FilterSet::from_query_params(¶ms, &allowed);
84
85 assert_eq!(fs.filters.len(), 2);
86 assert!(fs
87 .filters
88 .iter()
89 .any(|f| f.field == "role" && f.value == "admin"));
90 assert!(fs
91 .filters
92 .iter()
93 .any(|f| f.field == "org_id" && f.value == "abc-123"));
94 }
95
96 #[test]
97 fn filter_disallowed_fields_ignored() {
98 let mut params = HashMap::new();
99 params.insert("filter[secret]".to_string(), "value".to_string());
100
101 let allowed: Vec<String> = vec!["role".to_string()];
102 let fs = FilterSet::from_query_params(¶ms, &allowed);
103
104 assert!(fs.is_empty());
105 }
106
107 #[test]
108 fn apply_to_sql_no_existing_where() {
109 let fs = FilterSet {
110 filters: vec![
111 FilterParam {
112 field: "role".to_string(),
113 value: "admin".to_string(),
114 },
115 FilterParam {
116 field: "org_id".to_string(),
117 value: "abc".to_string(),
118 },
119 ],
120 };
121
122 let mut sql = "SELECT * FROM users".to_string();
123 let offset = fs.apply_to_sql(&mut sql, false, 1);
124
125 assert_eq!(
126 sql,
127 "SELECT * FROM users WHERE \"role\" = $1 AND \"org_id\" = $2"
128 );
129 assert_eq!(offset, 3);
130 }
131
132 #[test]
133 fn apply_to_sql_with_existing_where() {
134 let fs = FilterSet {
135 filters: vec![FilterParam {
136 field: "role".to_string(),
137 value: "admin".to_string(),
138 }],
139 };
140
141 let mut sql = "SELECT * FROM users WHERE \"deleted_at\" IS NULL".to_string();
142 let offset = fs.apply_to_sql(&mut sql, true, 1);
143
144 assert_eq!(
145 sql,
146 "SELECT * FROM users WHERE \"deleted_at\" IS NULL AND \"role\" = $1"
147 );
148 assert_eq!(offset, 2);
149 }
150
151 #[test]
152 fn empty_filter_set() {
153 let fs = FilterSet::default();
154 assert!(fs.is_empty());
155
156 let mut sql = "SELECT * FROM users".to_string();
157 let offset = fs.apply_to_sql(&mut sql, false, 1);
158 assert_eq!(sql, "SELECT * FROM users");
159 assert_eq!(offset, 1);
160 }
161}