1use std::collections::HashMap;
6use std::sync::{Arc, OnceLock};
7
8use helios_fhirpath::EvaluationContext;
9use helios_fhirpath_support::EvaluationResult;
10use parking_lot::RwLock;
11use regex::Regex;
12use rust_decimal::Decimal;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16use crate::types::SearchParamType;
17
18use super::converters::{IndexValue, ValueConverter};
19use super::errors::ExtractionError;
20use super::registry::{SearchParameterDefinition, SearchParameterRegistry};
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ExtractedValue {
25 pub param_name: String,
27
28 pub param_url: String,
30
31 pub param_type: SearchParamType,
33
34 pub value: IndexValue,
36
37 pub composite_group: Option<u32>,
40}
41
42impl ExtractedValue {
43 pub fn new(
45 param_name: impl Into<String>,
46 param_url: impl Into<String>,
47 param_type: SearchParamType,
48 value: IndexValue,
49 ) -> Self {
50 Self {
51 param_name: param_name.into(),
52 param_url: param_url.into(),
53 param_type,
54 value,
55 composite_group: None,
56 }
57 }
58
59 pub fn with_composite_group(mut self, group: u32) -> Self {
61 self.composite_group = Some(group);
62 self
63 }
64}
65
66#[derive(Debug, Clone)]
68pub struct ContainedExtraction {
69 pub contained_type: String,
71 pub local_id: String,
73 pub content: Value,
76 pub values: Vec<ExtractedValue>,
78}
79
80pub struct SearchParameterExtractor {
82 registry: Arc<RwLock<SearchParameterRegistry>>,
83}
84
85impl SearchParameterExtractor {
86 pub fn new(registry: Arc<RwLock<SearchParameterRegistry>>) -> Self {
88 Self { registry }
89 }
90
91 pub fn extract(
95 &self,
96 resource: &Value,
97 resource_type: &str,
98 ) -> Result<Vec<ExtractedValue>, ExtractionError> {
99 let obj = resource
101 .as_object()
102 .ok_or_else(|| ExtractionError::InvalidResource {
103 message: "Resource must be a JSON object".to_string(),
104 })?;
105
106 if let Some(rt) = obj.get("resourceType").and_then(|v| v.as_str()) {
108 if rt != resource_type {
109 return Err(ExtractionError::InvalidResource {
110 message: format!(
111 "Resource type mismatch: expected {}, got {}",
112 resource_type, rt
113 ),
114 });
115 }
116 }
117
118 let mut results = Vec::new();
119
120 let params = {
122 let registry = self.registry.read();
123 registry.get_active_params(resource_type)
124 };
125
126 for param in ¶ms {
127 match self.extract_for_param(resource, param) {
128 Ok(values) => results.extend(values),
129 Err(e) => {
130 tracing::warn!(
132 "Failed to extract values for parameter '{}': {}",
133 param.code,
134 e
135 );
136 }
137 }
138 }
139
140 let common_params = {
142 let registry = self.registry.read();
143 registry.get_active_params("Resource")
144 };
145
146 for param in &common_params {
147 if !params.iter().any(|p| p.code == param.code) {
148 match self.extract_for_param(resource, param) {
149 Ok(values) => results.extend(values),
150 Err(e) => {
151 tracing::warn!(
152 "Failed to extract values for common parameter '{}': {}",
153 param.code,
154 e
155 );
156 }
157 }
158 }
159 }
160
161 Ok(results)
162 }
163
164 pub fn extract_contained(&self, container: &Value) -> Vec<ContainedExtraction> {
173 let Some(entries) = container.get("contained").and_then(|c| c.as_array()) else {
174 return Vec::new();
175 };
176
177 let mut out = Vec::new();
178 for entry in entries {
179 let (Some(contained_type), Some(local_id)) = (
180 entry.get("resourceType").and_then(|v| v.as_str()),
181 entry.get("id").and_then(|v| v.as_str()),
182 ) else {
183 continue;
184 };
185 match self.extract(entry, contained_type) {
186 Ok(values) if !values.is_empty() => out.push(ContainedExtraction {
187 contained_type: contained_type.to_string(),
188 local_id: local_id.to_string(),
189 content: entry.clone(),
190 values,
191 }),
192 Ok(_) => {}
193 Err(e) => tracing::warn!(
194 "Failed to extract contained {}/{}: {}",
195 contained_type,
196 local_id,
197 e
198 ),
199 }
200 }
201 out
202 }
203
204 pub fn extract_for_param(
206 &self,
207 resource: &Value,
208 param: &SearchParameterDefinition,
209 ) -> Result<Vec<ExtractedValue>, ExtractionError> {
210 if matches!(param.param_type, SearchParamType::Composite) {
213 return self.extract_composite(resource, param);
214 }
215
216 if param.expression.is_empty() {
217 return Ok(Vec::new());
218 }
219
220 let resource_type = resource
222 .get("resourceType")
223 .and_then(|v| v.as_str())
224 .unwrap_or("");
225
226 let rewritten = rewrite_choice_types(¶m.expression);
229 let filtered_expr = self.filter_expression_for_resource(&rewritten, resource_type);
230
231 if filtered_expr.is_empty() {
232 return Ok(Vec::new());
233 }
234
235 let values = self.evaluate_fhirpath(resource, &filtered_expr)?;
237
238 let mut results = Vec::new();
239 for value in values {
240 let converted = ValueConverter::convert(&value, param.param_type, ¶m.code)?;
241 for idx_value in converted {
242 results.push(ExtractedValue::new(
243 ¶m.code,
244 ¶m.url,
245 param.param_type,
246 idx_value,
247 ));
248 }
249 }
250
251 Ok(results)
252 }
253
254 fn extract_composite(
263 &self,
264 resource: &Value,
265 param: &SearchParameterDefinition,
266 ) -> Result<Vec<ExtractedValue>, ExtractionError> {
267 let components = match ¶m.component {
268 Some(c) if !c.is_empty() => c,
269 _ => return Ok(Vec::new()),
270 };
271
272 let resource_type = resource
273 .get("resourceType")
274 .and_then(|v| v.as_str())
275 .unwrap_or("");
276
277 let rewritten_base = rewrite_choice_types(¶m.expression);
278 let base_expr = self.filter_expression_for_resource(&rewritten_base, resource_type);
279 if base_expr.is_empty() {
280 return Ok(Vec::new());
281 }
282
283 let component_types: Vec<Option<SearchParamType>> = {
285 let registry = self.registry.read();
286 components
287 .iter()
288 .map(|c| registry.get_by_url(&c.definition).map(|d| d.param_type))
289 .collect()
290 };
291
292 let base_nodes = self.evaluate_fhirpath(resource, &base_expr)?;
294
295 let mut results = Vec::new();
296 for (group_idx, node) in base_nodes.iter().enumerate() {
297 let group = group_idx as u32;
298 for (component, sub_type) in components.iter().zip(component_types.iter()) {
299 let sub_type = match sub_type {
300 Some(t) => *t,
301 None => continue, };
303 if component.expression.is_empty() {
304 continue;
305 }
306 let comp_expr = rewrite_choice_types(&component.expression);
307 let values = self.evaluate_fhirpath(node, &comp_expr)?;
308 for value in values {
309 let converted = ValueConverter::convert(&value, sub_type, ¶m.code)?;
310 for idx_value in converted {
311 results.push(
312 ExtractedValue::new(¶m.code, ¶m.url, sub_type, idx_value)
313 .with_composite_group(group),
314 );
315 }
316 }
317 }
318 }
319
320 Ok(results)
321 }
322
323 fn filter_expression_for_resource(&self, expression: &str, resource_type: &str) -> String {
332 let parts: Vec<String> = expression
334 .split('|')
335 .map(|p| p.trim())
336 .filter(|p| {
337 p.starts_with(resource_type)
339 && (p.len() == resource_type.len()
340 || p.chars().nth(resource_type.len()) == Some('.'))
341 })
342 .map(|p| self.simplify_resolve_pattern(p))
343 .collect();
344
345 if parts.is_empty() {
346 expression.to_string()
349 } else {
350 parts.join(" | ")
352 }
353 }
354
355 fn simplify_resolve_pattern(&self, expr: &str) -> String {
362 if let Some(where_pos) = expr.find(".where(resolve()") {
365 let after_where = &expr[where_pos..];
367 if after_where.rfind(')').is_some() {
368 return expr[..where_pos].to_string();
370 }
371 }
372 expr.to_string()
373 }
374
375 fn evaluate_fhirpath(
377 &self,
378 resource: &Value,
379 expression: &str,
380 ) -> Result<Vec<Value>, ExtractionError> {
381 let eval_result = json_to_evaluation_result(resource)?;
383
384 let mut context = EvaluationContext::new_empty_with_default_version();
386 context.set_this(eval_result);
387
388 let result = helios_fhirpath::evaluate_expression(expression, &context).map_err(|e| {
390 ExtractionError::FhirPathError {
391 expression: expression.to_string(),
392 message: e,
393 }
394 })?;
395
396 evaluation_result_to_json_values(&result)
398 }
399}
400
401fn rewrite_choice_types(expression: &str) -> String {
422 static AS_FN: OnceLock<Regex> = OnceLock::new();
423 static OF_TYPE: OnceLock<Regex> = OnceLock::new();
424 static PAREN_AS: OnceLock<Regex> = OnceLock::new();
425 static BARE_AS: OnceLock<Regex> = OnceLock::new();
426
427 let path = r"[A-Za-z_][A-Za-z0-9_.]*";
428 let ty = r"[A-Za-z][A-Za-z0-9]*";
429 let as_fn =
430 AS_FN.get_or_init(|| Regex::new(&format!(r"({path})\.as\(\s*({ty})\s*\)")).unwrap());
431 let of_type =
432 OF_TYPE.get_or_init(|| Regex::new(&format!(r"({path})\.ofType\(\s*({ty})\s*\)")).unwrap());
433 let paren_as =
434 PAREN_AS.get_or_init(|| Regex::new(&format!(r"\(\s*({path})\s+as\s+({ty})\s*\)")).unwrap());
435 let bare_as = BARE_AS.get_or_init(|| Regex::new(&format!(r"({path})\s+as\s+({ty})")).unwrap());
436
437 let concrete = |caps: ®ex::Captures| -> String {
438 let base = &caps[1];
439 let type_name = &caps[2];
440 let mut chars = type_name.chars();
441 let capitalized = match chars.next() {
442 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
443 None => String::new(),
444 };
445 format!("{}{}", base, capitalized)
446 };
447
448 let step1 = as_fn.replace_all(expression, &concrete);
451 let step2 = of_type.replace_all(&step1, &concrete);
452 let step3 = paren_as.replace_all(&step2, &concrete);
453 bare_as.replace_all(&step3, &concrete).into_owned()
454}
455
456fn json_to_evaluation_result(value: &Value) -> Result<EvaluationResult, ExtractionError> {
458 match value {
459 Value::Null => Ok(EvaluationResult::Empty),
460 Value::Bool(b) => Ok(EvaluationResult::boolean(*b)),
461 Value::Number(n) => {
462 if let Some(i) = n.as_i64() {
463 Ok(EvaluationResult::integer(i))
464 } else if let Some(f) = n.as_f64() {
465 Ok(EvaluationResult::decimal(Decimal::try_from(f).map_err(
466 |e| ExtractionError::ConversionError {
467 message: format!("Invalid decimal: {}", e),
468 },
469 )?))
470 } else {
471 Err(ExtractionError::ConversionError {
472 message: "Invalid number".to_string(),
473 })
474 }
475 }
476 Value::String(s) => Ok(EvaluationResult::string(s.clone())),
477 Value::Array(arr) => {
478 let results: Result<Vec<_>, _> = arr.iter().map(json_to_evaluation_result).collect();
479 Ok(EvaluationResult::collection(results?))
480 }
481 Value::Object(obj) => {
482 let mut map = HashMap::new();
483 for (key, val) in obj {
484 let eval_val = json_to_evaluation_result(val)?;
485 map.insert(key.clone(), eval_val);
486 }
487 Ok(EvaluationResult::Object {
488 map,
489 type_info: None,
490 })
491 }
492 }
493}
494
495fn evaluation_result_to_json_values(
497 result: &EvaluationResult,
498) -> Result<Vec<Value>, ExtractionError> {
499 match result {
500 EvaluationResult::Empty => Ok(Vec::new()),
501 EvaluationResult::Boolean(b, _, _) => Ok(vec![Value::Bool(*b)]),
502 EvaluationResult::String(s, _, _) => Ok(vec![Value::String(s.clone())]),
503 EvaluationResult::Integer(i, _, _) => Ok(vec![Value::Number((*i).into())]),
504 EvaluationResult::Integer64(i, _, _) => Ok(vec![Value::Number((*i).into())]),
505 EvaluationResult::Decimal(d, _, _) => {
506 let f: f64 = (*d).try_into().unwrap_or(0.0);
508 Ok(vec![Value::Number(
509 serde_json::Number::from_f64(f).unwrap_or_else(|| serde_json::Number::from(0)),
510 )])
511 }
512 EvaluationResult::Date(s, _, _) => Ok(vec![Value::String(s.clone())]),
513 EvaluationResult::DateTime(s, _, _) => Ok(vec![Value::String(s.clone())]),
514 EvaluationResult::Time(s, _, _) => Ok(vec![Value::String(s.clone())]),
515 EvaluationResult::Quantity(value, unit, _, _) => {
516 let f: f64 = (*value).try_into().unwrap_or(0.0);
518 Ok(vec![serde_json::json!({
519 "value": f,
520 "unit": unit
521 })])
522 }
523 EvaluationResult::Collection { items, .. } => {
524 let mut values = Vec::new();
525 for item in items {
526 values.extend(evaluation_result_to_json_values(item)?);
527 }
528 Ok(values)
529 }
530 EvaluationResult::Object { map, .. } => {
531 let mut obj = serde_json::Map::new();
533 for (key, val) in map {
534 let json_vals = evaluation_result_to_json_values(val)?;
535 let is_collection = matches!(val, EvaluationResult::Collection { .. });
538 if is_collection {
539 obj.insert(key.clone(), Value::Array(json_vals));
541 } else if json_vals.len() == 1 {
542 obj.insert(key.clone(), json_vals.into_iter().next().unwrap());
543 } else if !json_vals.is_empty() {
544 obj.insert(key.clone(), Value::Array(json_vals));
545 }
546 }
547 Ok(vec![Value::Object(obj)])
548 }
549 }
550}
551
552impl std::fmt::Debug for SearchParameterExtractor {
553 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
554 f.debug_struct("SearchParameterExtractor").finish()
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561 use crate::search::loader::SearchParameterLoader;
562 use helios_fhir::FhirVersion;
563 use serde_json::json;
564 use std::path::PathBuf;
565
566 #[test]
567 fn rewrite_choice_types_handles_all_forms() {
568 assert_eq!(rewrite_choice_types("value.as(Quantity)"), "valueQuantity");
570 assert_eq!(
572 rewrite_choice_types("(Observation.value.ofType(Quantity))"),
573 "(Observation.valueQuantity)"
574 );
575 assert_eq!(
577 rewrite_choice_types("(Observation.value as Quantity)"),
578 "Observation.valueQuantity"
579 );
580 assert_eq!(
582 rewrite_choice_types("(RiskAssessment.occurrence as dateTime)"),
583 "RiskAssessment.occurrenceDateTime"
584 );
585 assert_eq!(
587 rewrite_choice_types("value.as(Quantity) | value.as(Range)"),
588 "valueQuantity | valueRange"
589 );
590 assert_eq!(rewrite_choice_types("Observation.code"), "Observation.code");
591 }
592
593 fn create_test_extractor() -> SearchParameterExtractor {
594 let loader = SearchParameterLoader::new(FhirVersion::R4);
595 let mut registry = SearchParameterRegistry::new();
596
597 if let Ok(params) = loader.load_embedded() {
599 for param in params {
600 let _ = registry.register(param);
601 }
602 }
603
604 let data_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
608 .parent()
609 .and_then(|p| p.parent())
610 .map(|p| p.join("data"))
611 .unwrap_or_else(|| PathBuf::from("data"));
612
613 if let Ok(params) = loader.load_from_spec_file(&data_dir) {
614 for param in params {
615 let _ = registry.register(param);
616 }
617 }
618
619 SearchParameterExtractor::new(Arc::new(RwLock::new(registry)))
620 }
621
622 #[test]
623 fn test_extract_patient_name() {
624 let extractor = create_test_extractor();
625
626 let patient = json!({
627 "resourceType": "Patient",
628 "id": "123",
629 "name": [
630 {
631 "family": "Smith",
632 "given": ["John", "James"]
633 }
634 ]
635 });
636
637 let values = extractor.extract(&patient, "Patient").unwrap();
638
639 let name_values: Vec<_> = values.iter().filter(|v| v.param_name == "name").collect();
641 assert!(!name_values.is_empty(), "Should extract 'name' values");
642
643 let family_values: Vec<_> = values.iter().filter(|v| v.param_name == "family").collect();
645 assert!(!family_values.is_empty(), "Should extract 'family' values");
646 }
647
648 #[test]
649 fn test_extract_patient_identifier() {
650 let extractor = create_test_extractor();
651
652 let patient = json!({
653 "resourceType": "Patient",
654 "id": "123",
655 "identifier": [
656 {
657 "system": "http://hospital.org/mrn",
658 "value": "12345"
659 }
660 ]
661 });
662
663 let values = extractor.extract(&patient, "Patient").unwrap();
664
665 let id_values: Vec<_> = values
666 .iter()
667 .filter(|v| v.param_name == "identifier")
668 .collect();
669 assert!(!id_values.is_empty(), "Should extract 'identifier' values");
670
671 if let IndexValue::Token { system, code, .. } = &id_values[0].value {
672 assert_eq!(system.as_ref().unwrap(), "http://hospital.org/mrn");
673 assert_eq!(code, "12345");
674 }
675 }
676
677 #[test]
678 fn test_extract_observation_values() {
679 let extractor = create_test_extractor();
680
681 let observation = json!({
682 "resourceType": "Observation",
683 "id": "obs1",
684 "code": {
685 "coding": [
686 {
687 "system": "http://loinc.org",
688 "code": "8867-4"
689 }
690 ]
691 },
692 "subject": {
693 "reference": "Patient/123"
694 },
695 "valueQuantity": {
696 "value": 120.5,
697 "unit": "mmHg"
698 }
699 });
700
701 let values = extractor.extract(&observation, "Observation").unwrap();
702
703 let code_values: Vec<_> = values.iter().filter(|v| v.param_name == "code").collect();
705 assert!(!code_values.is_empty(), "Should extract 'code' values");
706
707 let subject_values: Vec<_> = values
709 .iter()
710 .filter(|v| v.param_name == "subject")
711 .collect();
712 assert!(
713 !subject_values.is_empty(),
714 "Should extract 'subject' values"
715 );
716 }
717
718 #[test]
719 fn test_invalid_resource() {
720 let extractor = create_test_extractor();
721
722 let not_object = json!("string");
723 let result = extractor.extract(¬_object, "Patient");
724 assert!(result.is_err());
725 }
726
727 #[test]
728 fn test_resource_type_mismatch() {
729 let extractor = create_test_extractor();
730
731 let patient = json!({
732 "resourceType": "Patient",
733 "id": "123"
734 });
735
736 let result = extractor.extract(&patient, "Observation");
737 assert!(result.is_err());
738 }
739
740 #[test]
741 fn test_fhirpath_with_where_clause() {
742 let extractor = create_test_extractor();
743
744 let patient = json!({
746 "resourceType": "Patient",
747 "id": "123",
748 "name": [
749 {
750 "use": "official",
751 "family": "Smith",
752 "given": ["John"]
753 },
754 {
755 "use": "nickname",
756 "given": ["Johnny"]
757 }
758 ]
759 });
760
761 let values = extractor.extract(&patient, "Patient").unwrap();
762
763 let name_values: Vec<_> = values.iter().filter(|v| v.param_name == "name").collect();
765 assert!(
766 name_values.len() >= 2,
767 "Should extract multiple name values"
768 );
769 }
770
771 #[test]
772 fn test_extract_observation_code_with_display() {
773 let extractor = create_test_extractor();
774
775 let observation = json!({
776 "resourceType": "Observation",
777 "id": "obs1",
778 "status": "final",
779 "code": {
780 "coding": [
781 {
782 "system": "http://loinc.org",
783 "code": "8867-4",
784 "display": "Heart rate"
785 }
786 ]
787 }
788 });
789
790 let values = extractor.extract(&observation, "Observation").unwrap();
792
793 let code_values: Vec<_> = values.iter().filter(|v| v.param_name == "code").collect();
795 assert!(!code_values.is_empty(), "Should extract 'code' values");
796
797 if let Some(first_code) = code_values.first() {
799 if let IndexValue::Token { display, .. } = &first_code.value {
800 assert_eq!(
801 display.as_deref(),
802 Some("Heart rate"),
803 "Display should be populated"
804 );
805 }
806 }
807 }
808
809 #[test]
810 fn test_extract_resource_id() {
811 let extractor = create_test_extractor();
812
813 let patient = json!({
814 "resourceType": "Patient",
815 "id": "p1"
816 });
817
818 let values = extractor.extract(&patient, "Patient").unwrap();
819
820 let id_values: Vec<_> = values.iter().filter(|v| v.param_name == "_id").collect();
822 assert!(!id_values.is_empty(), "Should extract '_id' parameter");
823
824 if let Some(first_id) = id_values.first() {
826 if let IndexValue::Token { code, .. } = &first_id.value {
827 assert_eq!(code, "p1", "_id should be 'p1'");
828 }
829 }
830 }
831
832 #[test]
833 fn test_json_to_evaluation_result() {
834 assert!(matches!(
836 json_to_evaluation_result(&json!(null)).unwrap(),
837 EvaluationResult::Empty
838 ));
839
840 assert!(matches!(
841 json_to_evaluation_result(&json!(true)).unwrap(),
842 EvaluationResult::Boolean(true, _, _)
843 ));
844
845 assert!(matches!(
846 json_to_evaluation_result(&json!("test")).unwrap(),
847 EvaluationResult::String(s, _, _) if s == "test"
848 ));
849
850 assert!(matches!(
851 json_to_evaluation_result(&json!(42)).unwrap(),
852 EvaluationResult::Integer(42, _, _)
853 ));
854
855 if let EvaluationResult::Collection { items, .. } =
857 json_to_evaluation_result(&json!([1, 2, 3])).unwrap()
858 {
859 assert_eq!(items.len(), 3);
860 } else {
861 panic!("Expected collection");
862 }
863
864 if let EvaluationResult::Object { map, .. } =
866 json_to_evaluation_result(&json!({"key": "value"})).unwrap()
867 {
868 assert!(map.contains_key("key"));
869 } else {
870 panic!("Expected object");
871 }
872 }
873
874 #[test]
875 fn test_filter_expression_for_resource() {
876 let extractor = create_test_extractor();
877
878 let complex_expr =
880 "AllergyIntolerance.patient | Immunization.patient | Observation.subject";
881 let filtered = extractor.filter_expression_for_resource(complex_expr, "Immunization");
882 assert_eq!(filtered, "Immunization.patient");
883
884 let no_match = extractor.filter_expression_for_resource(complex_expr, "Patient");
886 assert_eq!(no_match, complex_expr);
887
888 let simple_expr = "Patient.name";
890 let simple_filtered = extractor.filter_expression_for_resource(simple_expr, "Patient");
891 assert_eq!(simple_filtered, "Patient.name");
892
893 let partial = extractor.filter_expression_for_resource("Observation.code", "Obs");
895 assert_eq!(partial, "Observation.code");
896
897 let with_resolve = "Observation.subject.where(resolve() is Patient) | Patient.link.other";
899 let stripped = extractor.filter_expression_for_resource(with_resolve, "Observation");
900 assert_eq!(stripped, "Observation.subject");
901
902 let patient_expr = "CarePlan.subject.where(resolve() is Patient) | Observation.subject.where(resolve() is Patient)";
904 let careplan_filtered = extractor.filter_expression_for_resource(patient_expr, "CarePlan");
905 assert_eq!(careplan_filtered, "CarePlan.subject");
906 let obs_filtered = extractor.filter_expression_for_resource(patient_expr, "Observation");
907 assert_eq!(obs_filtered, "Observation.subject");
908 }
909
910 #[test]
911 fn test_extract_immunization_patient() {
912 let extractor = create_test_extractor();
913
914 let immunization = json!({
915 "resourceType": "Immunization",
916 "id": "test-imm",
917 "status": "completed",
918 "vaccineCode": {
919 "coding": [{
920 "system": "http://hl7.org/fhir/sid/cvx",
921 "code": "140"
922 }]
923 },
924 "patient": {
925 "reference": "Patient/test-patient"
926 },
927 "occurrenceDateTime": "2021-01-01"
928 });
929
930 let values = extractor.extract(&immunization, "Immunization").unwrap();
931
932 let patient_values: Vec<_> = values
934 .iter()
935 .filter(|v| v.param_name == "patient")
936 .collect();
937 assert!(
938 !patient_values.is_empty(),
939 "Should extract 'patient' values from Immunization"
940 );
941
942 if let IndexValue::Reference { reference, .. } = &patient_values[0].value {
944 assert!(
945 reference.contains("Patient/test-patient") || reference.contains("test-patient"),
946 "Should contain patient reference, got: {}",
947 reference
948 );
949 }
950 }
951}