1use std::sync::Arc;
2
3use haste_fhir_client::url::{ParsedParameter, ParsedParameters};
4use haste_fhir_model::r4::generated::{
5 resources::{Resource, ResourceType, Subscription},
6 terminology::IssueType,
7};
8use haste_fhir_operation_error::{OperationOutcomeError, derive::OperationOutcomeError};
9use haste_fhir_search::{
10 ResolvedParameter, SearchParameterResolve,
11 indexing_conversion::{self, InsertableIndex},
12};
13use haste_jwt::{ProjectId, TenantId};
14
15pub mod traits;
16
17#[derive(OperationOutcomeError, Debug)]
18pub enum SubscriptionFilterError {
19 #[fatal(
20 code = "exception",
21 diagnostic = "Failed to evaluate fhirpath expression."
22 )]
23 FHIRPathError(#[from] haste_fhirpath::FHIRPathError),
24}
25
26#[allow(dead_code)]
27pub struct SubscriptionParameter {
28 parameter: ResolvedParameter,
29 fp_extract_expression: String,
30 value: Vec<String>,
31 modifier: Option<String>,
32}
33
34pub enum SubscriptionTrigger {
35 QueryFilter {
37 resource_type: ResourceType,
38 parameters: Vec<SubscriptionParameter>,
39 },
40 FHIRPathFilter {
43 expression: String,
44 },
45}
46
47#[allow(dead_code)]
50pub struct MemorySubscriptionFilter {
51 fp_engine: haste_fhirpath::FPEngine,
52 triggers: Vec<SubscriptionTrigger>,
53}
54
55impl MemorySubscriptionFilter {
56 pub async fn new<Resolver: SearchParameterResolve>(
57 tenant_id: &TenantId,
58 project_id: &ProjectId,
59 resolver: Arc<Resolver>,
60 value: Subscription,
61 ) -> Result<Self, OperationOutcomeError> {
62 if let Some(criteria) = value.criteria.value {
63 let criteria_pieces = criteria.split('?').collect::<Vec<_>>();
64 let [path, parameters] = criteria_pieces.as_slice() else {
65 return Err(OperationOutcomeError::error(
66 IssueType::Exception(None),
67 "Invalid subscription criteria format".to_string(),
68 ));
69 };
70
71 let resource_type = ResourceType::try_from(*path).map_err(|_| {
72 OperationOutcomeError::error(
73 IssueType::Exception(None),
74 "Invalid resource type".to_string(),
75 )
76 })?;
77
78 let parsed_parameters = ParsedParameters::try_from(*parameters)?;
79 let mut subscription_parsed_parameters = vec![];
80
81 for parameter in parsed_parameters.owned_parameters().into_iter() {
82 match parameter {
83 ParsedParameter::Resource(resource_param) => {
84 let Some(parameter) = resolver
85 .by_name(
86 tenant_id,
87 project_id,
88 Some(&resource_type),
89 &resource_param.name,
90 )
91 .await?
92 else {
93 return Err(OperationOutcomeError::error(
94 IssueType::Exception(None),
95 format!(
96 "Invalid search parameter in subscription criteria: {}",
97 resource_param.name
98 ),
99 ));
100 };
101
102 if resource_param.chains.is_some() {
103 return Err(OperationOutcomeError::error(
104 IssueType::Exception(None),
105 format!(
106 "Chained parameters are not supported in subscription criteria: {}",
107 resource_param.name
108 ),
109 ));
110 }
111
112 let Some(fp_expression) = parameter
113 .search_parameter
114 .expression
115 .as_ref()
116 .and_then(|expr| expr.value.as_ref())
117 else {
118 return Err(OperationOutcomeError::error(
119 IssueType::Exception(None),
120 format!(
121 "Search parameter does not have an expression: {}",
122 resource_param.name
123 ),
124 ));
125 };
126
127 subscription_parsed_parameters.push(SubscriptionParameter {
128 parameter: parameter.clone(),
129 fp_extract_expression: fp_expression.clone(),
130 value: resource_param.value,
131 modifier: resource_param.modifier,
132 });
133 }
134 ParsedParameter::Result(result_param) => {
135 return Err(OperationOutcomeError::error(
136 IssueType::Exception(None),
137 format!(
138 "Unsupported parameter in subscription criteria: {}",
139 result_param.name
140 ),
141 ));
142 }
143 }
144 }
145
146 Ok(MemorySubscriptionFilter {
147 fp_engine: haste_fhirpath::FPEngine::new(),
148 triggers: vec![SubscriptionTrigger::QueryFilter {
149 resource_type,
150 parameters: subscription_parsed_parameters,
151 }],
152 })
153 } else {
154 Err(OperationOutcomeError::error(
155 IssueType::Exception(None),
156 "SubscriptionFilter conversion not implemented".to_string(),
157 ))
158 }
159 }
160}
161
162async fn fits_subscription_parameter(
163 fp_engine: &haste_fhirpath::FPEngine,
164 subscription_parameter: &SubscriptionParameter,
165 resource: &Resource,
166) -> Result<bool, OperationOutcomeError> {
167 let result = fp_engine
168 .evaluate(
169 &subscription_parameter.fp_extract_expression,
170 vec![resource],
171 )
172 .await
173 .map_err(SubscriptionFilterError::from)?;
174
175 let conversions = indexing_conversion::to_insertable_index(
176 &subscription_parameter.parameter,
177 result.iter().collect::<Vec<_>>(),
178 )?;
179
180 match conversions {
181 InsertableIndex::String(resource_values) => {
182 Ok(resource_values.iter().any(|resource_value| {
183 subscription_parameter
184 .value
185 .iter()
186 .any(|v| resource_value.to_lowercase().starts_with(&v.to_lowercase()))
187 }))
188 }
189 InsertableIndex::Number(_) => Err(OperationOutcomeError::error(
190 IssueType::Exception(None),
191 "Number search parameters are not supported in subscription criteria".to_string(),
192 ))?,
193 InsertableIndex::URI(_) => Err(OperationOutcomeError::error(
194 IssueType::Exception(None),
195 "URI search parameters are not supported in subscription criteria".to_string(),
196 ))?,
197 InsertableIndex::Token(_) => Err(OperationOutcomeError::error(
198 IssueType::Exception(None),
199 "Token search parameters are not supported in subscription criteria".to_string(),
200 ))?,
201 InsertableIndex::Date(_) => Err(OperationOutcomeError::error(
202 IssueType::Exception(None),
203 "Date search parameters are not supported in subscription criteria".to_string(),
204 ))?,
205
206 InsertableIndex::Reference(_) => Err(OperationOutcomeError::error(
207 IssueType::Exception(None),
208 "Reference search parameters are not supported in subscription criteria".to_string(),
209 ))?,
210 InsertableIndex::Quantity(_) => Err(OperationOutcomeError::error(
211 IssueType::Exception(None),
212 "Quantity search parameters are not supported in subscription criteria".to_string(),
213 ))?,
214 InsertableIndex::DynamicParameters(_) => Err(OperationOutcomeError::error(
215 IssueType::Exception(None),
216 "Dynamic search parameters are not supported in subscription criteria".to_string(),
217 ))?,
218
219 InsertableIndex::Composite(_) => Err(OperationOutcomeError::error(
220 IssueType::Exception(None),
221 "Composite search parameters are not supported in subscription criteria".to_string(),
222 ))?,
223 InsertableIndex::Special(_) => Err(OperationOutcomeError::error(
224 IssueType::Exception(None),
225 "Special search parameters are not supported in subscription criteria".to_string(),
226 ))?,
227 InsertableIndex::Meta(_) => Err(OperationOutcomeError::error(
228 IssueType::Exception(None),
229 "Meta search parameters are not supported in subscription criteria".to_string(),
230 ))?,
231 }
232}
233
234impl traits::SubscriptionFilter for MemorySubscriptionFilter {
235 async fn matches(&self, resource: &Resource) -> Result<bool, OperationOutcomeError> {
236 let resource_resource_type = resource.resource_type();
237
238 for trigger in self.triggers.iter() {
239 match trigger {
240 SubscriptionTrigger::QueryFilter {
241 resource_type,
242 parameters,
243 } => {
244 if *resource_type != resource_resource_type {
245 return Ok(false);
246 }
247
248 for sub_parameter in parameters {
249 let fits_criteria =
250 fits_subscription_parameter(&self.fp_engine, sub_parameter, resource)
251 .await?;
252 if !fits_criteria {
253 return Ok(false);
254 }
255 }
256
257 return Ok(true);
258 }
259 SubscriptionTrigger::FHIRPathFilter { .. } => {
260 return Err(OperationOutcomeError::error(
261 IssueType::Exception(None),
262 "FHIRPathFilter triggers are not yet supported".to_string(),
263 ))?;
264 }
265 }
266 }
267
268 Ok(false)
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use haste_fhir_model::r4::generated::{
275 resources::Patient,
276 types::{FHIRString, HumanName},
277 };
278 use haste_fhir_search::memory::R4_SEARCH_PARAMETERS_INDEX;
279
280 use crate::traits::SubscriptionFilter;
281
282 use super::*;
283
284 #[tokio::test]
285 async fn quick_test_derive() {
286 let subscription = Subscription {
287 criteria: Box::new(FHIRString {
288 value: Some("Patient?name=Smith".to_string()),
289 ..Default::default()
290 }),
291 ..Default::default()
292 };
293
294 let resolver = R4_SEARCH_PARAMETERS_INDEX.clone();
295
296 let sub_filter = MemorySubscriptionFilter::new(
297 &TenantId::System,
298 &ProjectId::System,
299 resolver,
300 subscription,
301 )
302 .await
303 .unwrap();
304
305 assert_eq!(sub_filter.triggers.len(), 1);
306
307 match &sub_filter.triggers[0] {
308 SubscriptionTrigger::QueryFilter {
309 resource_type,
310 parameters,
311 } => {
312 assert_eq!(resource_type, &ResourceType::Patient);
313 assert_eq!(parameters[0].fp_extract_expression, "Patient.name");
314 assert_eq!(parameters[0].value, vec!["Smith".to_string()]);
315 }
316 _ => panic!("Expected QueryFilter trigger"),
317 };
318 }
319
320 #[tokio::test]
321 async fn modifier_check() {
322 let subscription = Subscription {
323 criteria: Box::new(FHIRString {
324 value: Some("Observation?category:missing=true".to_string()),
325 ..Default::default()
326 }),
327 ..Default::default()
328 };
329
330 let resolver = R4_SEARCH_PARAMETERS_INDEX.clone();
331 let sub_filter = MemorySubscriptionFilter::new(
332 &TenantId::System,
333 &ProjectId::System,
334 resolver,
335 subscription,
336 )
337 .await
338 .unwrap();
339
340 assert_eq!(sub_filter.triggers.len(), 1);
341
342 match &sub_filter.triggers[0] {
343 SubscriptionTrigger::QueryFilter {
344 resource_type,
345 parameters,
346 } => {
347 assert_eq!(resource_type, &ResourceType::Observation);
348 assert_eq!(parameters[0].fp_extract_expression, "Observation.category");
349 assert_eq!(parameters[0].value, vec!["true".to_string()]);
350 assert_eq!(parameters[0].modifier, Some("missing".to_string()));
351 }
352 _ => panic!("Expected QueryFilter trigger"),
353 };
354 }
355
356 #[tokio::test]
357 async fn test_run_fhirpath() {
358 let resolver = R4_SEARCH_PARAMETERS_INDEX.clone();
359 let sub_filter = MemorySubscriptionFilter::new(
360 &TenantId::System,
361 &ProjectId::System,
362 resolver.clone(),
363 Subscription {
364 criteria: Box::new(FHIRString {
365 value: Some("Patient?name=Smith".to_string()),
366 ..Default::default()
367 }),
368 ..Default::default()
369 },
370 )
371 .await
372 .unwrap();
373 let patient = Resource::Patient(Patient {
374 name: Some(vec![Box::new(HumanName {
375 family: Some(Box::new(FHIRString {
376 value: Some("Smith".to_string()),
377 ..Default::default()
378 })),
379 ..Default::default()
380 })]),
381 ..Default::default()
382 });
383
384 assert_eq!(sub_filter.matches(&patient).await.unwrap(), true);
385
386 let sub_filter_partial = MemorySubscriptionFilter::new(
387 &TenantId::System,
388 &ProjectId::System,
389 resolver.clone(),
390 Subscription {
391 criteria: Box::new(FHIRString {
392 value: Some("Patient?name=Sm".to_string()),
393 ..Default::default()
394 }),
395 ..Default::default()
396 },
397 )
398 .await
399 .unwrap();
400
401 assert_eq!(sub_filter_partial.matches(&patient).await.unwrap(), true);
402
403 let sub_filter_casing = MemorySubscriptionFilter::new(
404 &TenantId::System,
405 &ProjectId::System,
406 resolver.clone(),
407 Subscription {
408 criteria: Box::new(FHIRString {
409 value: Some("Patient?name=sm".to_string()),
410 ..Default::default()
411 }),
412 ..Default::default()
413 },
414 )
415 .await
416 .unwrap();
417
418 assert_eq!(sub_filter_casing.matches(&patient).await.unwrap(), true);
419
420 let patient = Resource::Patient(Patient {
421 name: Some(vec![Box::new(HumanName {
422 family: Some(Box::new(FHIRString {
423 value: Some("NotSmith".to_string()),
424 ..Default::default()
425 })),
426 ..Default::default()
427 })]),
428 ..Default::default()
429 });
430
431 assert_eq!(sub_filter.matches(&patient).await.unwrap(), false);
432 }
433}