helios_subscriptions/manager/
filters.rs1use crate::error::SubscriptionError;
4use crate::topics::FilterDefinition;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct SubscriptionFilter {
9 pub resource_type: Option<String>,
11
12 pub filter_parameter: String,
14
15 pub comparator: String,
17
18 pub value: String,
20}
21
22pub fn parse_filter_string(filter: &str) -> Result<SubscriptionFilter, SubscriptionError> {
31 let (resource_type, param_str) = if let Some(idx) = filter.find('?') {
32 (Some(filter[..idx].to_string()), &filter[idx + 1..])
33 } else {
34 (None, filter)
35 };
36
37 let (param_name, value_str) =
38 param_str
39 .split_once('=')
40 .ok_or_else(|| SubscriptionError::InvalidFilter {
41 message: format!("filter missing '=' separator: {filter}"),
42 })?;
43
44 if param_name.is_empty() {
45 return Err(SubscriptionError::InvalidFilter {
46 message: "filter parameter name is empty".to_string(),
47 });
48 }
49
50 if value_str.is_empty() {
51 return Err(SubscriptionError::InvalidFilter {
52 message: format!("filter value is empty for parameter '{param_name}'"),
53 });
54 }
55
56 let (comparator, value) = if let Some((comp, val)) = value_str.split_once(':') {
58 if is_known_comparator(comp) {
60 (comp.to_string(), val.to_string())
61 } else {
62 ("eq".to_string(), value_str.to_string())
64 }
65 } else {
66 ("eq".to_string(), value_str.to_string())
67 };
68
69 Ok(SubscriptionFilter {
70 resource_type,
71 filter_parameter: param_name.to_string(),
72 comparator,
73 value,
74 })
75}
76
77fn is_known_comparator(s: &str) -> bool {
78 matches!(
79 s,
80 "eq" | "ne" | "gt" | "lt" | "ge" | "le" | "sa" | "eb" | "in" | "not-in"
81 )
82}
83
84pub fn validate_filters(
88 filters: &[SubscriptionFilter],
89 can_filter_by: &[FilterDefinition],
90) -> Result<(), SubscriptionError> {
91 for filter in filters {
92 let matching_def = can_filter_by.iter().find(|def| {
93 def.filter_parameter == filter.filter_parameter
95 && def
97 .resource_type
98 .as_ref()
99 .is_none_or(|rt| filter.resource_type.as_ref().is_none_or(|frt| frt == rt))
100 });
101
102 match matching_def {
103 None => {
104 return Err(SubscriptionError::InvalidFilter {
105 message: format!(
106 "filter parameter '{}' is not supported by the topic",
107 filter.filter_parameter
108 ),
109 });
110 }
111 Some(def) => {
112 if !def.comparators.is_empty() && !def.comparators.contains(&filter.comparator) {
114 return Err(SubscriptionError::InvalidFilter {
115 message: format!(
116 "comparator '{}' not supported for filter parameter '{}' (supported: {:?})",
117 filter.comparator, filter.filter_parameter, def.comparators
118 ),
119 });
120 }
121 }
122 }
123 }
124
125 Ok(())
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn test_parse_filter_with_resource_type() {
134 let filter = parse_filter_string("Observation?code=http://loinc.org|1234-5").unwrap();
135 assert_eq!(filter.resource_type.as_deref(), Some("Observation"));
136 assert_eq!(filter.filter_parameter, "code");
137 assert_eq!(filter.comparator, "eq");
138 assert_eq!(filter.value, "http://loinc.org|1234-5");
139 }
140
141 #[test]
142 fn test_parse_filter_without_resource_type() {
143 let filter = parse_filter_string("code=http://loinc.org|1234-5").unwrap();
144 assert!(filter.resource_type.is_none());
145 assert_eq!(filter.filter_parameter, "code");
146 assert_eq!(filter.value, "http://loinc.org|1234-5");
147 }
148
149 #[test]
150 fn test_parse_filter_with_comparator() {
151 let filter = parse_filter_string("Observation?value-quantity=gt:5").unwrap();
152 assert_eq!(filter.comparator, "gt");
153 assert_eq!(filter.value, "5");
154 }
155
156 #[test]
157 fn test_parse_filter_colon_in_value_not_comparator() {
158 let filter = parse_filter_string("code=http://loinc.org|1234-5").unwrap();
160 assert_eq!(filter.comparator, "eq");
161 assert_eq!(filter.value, "http://loinc.org|1234-5");
162 }
163
164 #[test]
165 fn test_parse_filter_reference() {
166 let filter = parse_filter_string("Encounter?patient=Patient/123").unwrap();
167 assert_eq!(filter.filter_parameter, "patient");
168 assert_eq!(filter.value, "Patient/123");
169 }
170
171 #[test]
172 fn test_parse_filter_missing_equals() {
173 let result = parse_filter_string("Observation?code");
174 assert!(result.is_err());
175 }
176
177 #[test]
178 fn test_parse_filter_empty_param() {
179 let result = parse_filter_string("=value");
180 assert!(result.is_err());
181 }
182
183 #[test]
184 fn test_parse_filter_empty_value() {
185 let result = parse_filter_string("code=");
186 assert!(result.is_err());
187 }
188
189 #[test]
190 fn test_validate_filters_success() {
191 let filters = vec![SubscriptionFilter {
192 resource_type: Some("Observation".to_string()),
193 filter_parameter: "code".to_string(),
194 comparator: "eq".to_string(),
195 value: "http://loinc.org|1234-5".to_string(),
196 }];
197
198 let can_filter_by = vec![FilterDefinition {
199 resource_type: Some("Observation".to_string()),
200 filter_parameter: "code".to_string(),
201 comparators: vec!["eq".to_string(), "in".to_string()],
202 modifiers: vec![],
203 }];
204
205 assert!(validate_filters(&filters, &can_filter_by).is_ok());
206 }
207
208 #[test]
209 fn test_validate_filters_unknown_parameter() {
210 let filters = vec![SubscriptionFilter {
211 resource_type: None,
212 filter_parameter: "unknown-param".to_string(),
213 comparator: "eq".to_string(),
214 value: "value".to_string(),
215 }];
216
217 let can_filter_by = vec![FilterDefinition {
218 resource_type: Some("Observation".to_string()),
219 filter_parameter: "code".to_string(),
220 comparators: vec!["eq".to_string()],
221 modifiers: vec![],
222 }];
223
224 let result = validate_filters(&filters, &can_filter_by);
225 assert!(result.is_err());
226 }
227
228 #[test]
229 fn test_validate_filters_unsupported_comparator() {
230 let filters = vec![SubscriptionFilter {
231 resource_type: None,
232 filter_parameter: "code".to_string(),
233 comparator: "gt".to_string(),
234 value: "value".to_string(),
235 }];
236
237 let can_filter_by = vec![FilterDefinition {
238 resource_type: Some("Observation".to_string()),
239 filter_parameter: "code".to_string(),
240 comparators: vec!["eq".to_string()],
241 modifiers: vec![],
242 }];
243
244 let result = validate_filters(&filters, &can_filter_by);
245 assert!(result.is_err());
246 }
247
248 #[test]
249 fn test_validate_filters_empty_comparators_allows_any() {
250 let filters = vec![SubscriptionFilter {
251 resource_type: None,
252 filter_parameter: "code".to_string(),
253 comparator: "gt".to_string(),
254 value: "value".to_string(),
255 }];
256
257 let can_filter_by = vec![FilterDefinition {
258 resource_type: None,
259 filter_parameter: "code".to_string(),
260 comparators: vec![], modifiers: vec![],
262 }];
263
264 assert!(validate_filters(&filters, &can_filter_by).is_ok());
265 }
266
267 #[test]
268 fn test_validate_empty_filters_always_valid() {
269 assert!(validate_filters(&[], &[]).is_ok());
270 }
271}