helios_sof/
reference_collector.rs1use std::collections::BTreeSet;
14
15use serde_json::Value;
16
17use crate::SofBundle;
18
19pub fn collect_reference_strings(bundle: &SofBundle) -> Vec<String> {
21 let mut out = BTreeSet::new();
22 if let Some(json) = bundle_to_json(bundle) {
23 collect_into(&json, &mut out);
24 }
25 out.into_iter().collect()
26}
27
28pub fn collect_references_from_json(value: &Value) -> Vec<String> {
31 let mut out = BTreeSet::new();
32 collect_into(value, &mut out);
33 out.into_iter().collect()
34}
35
36pub fn collect_reference_strings_from_resources(resources: &[Value]) -> Vec<String> {
39 let mut out = BTreeSet::new();
40 for resource in resources {
41 collect_into(resource, &mut out);
42 }
43 out.into_iter().collect()
44}
45
46pub fn collect_resource_keys_from_resources(resources: &[Value]) -> BTreeSet<String> {
48 let mut keys = BTreeSet::new();
49 for resource in resources {
50 if let (Some(rt), Some(id)) = (
51 resource.get("resourceType").and_then(Value::as_str),
52 resource.get("id").and_then(Value::as_str),
53 ) {
54 keys.insert(format!("{rt}/{id}"));
55 }
56 }
57 keys
58}
59
60pub fn collect_resource_keys(bundle: &SofBundle) -> BTreeSet<String> {
63 let mut keys = BTreeSet::new();
64 let Some(json) = bundle_to_json(bundle) else {
65 return keys;
66 };
67 let Some(entries) = json.get("entry").and_then(Value::as_array) else {
68 return keys;
69 };
70 for entry in entries {
71 let resource = entry.get("resource").unwrap_or(entry);
72 if let (Some(rt), Some(id)) = (
73 resource.get("resourceType").and_then(Value::as_str),
74 resource.get("id").and_then(Value::as_str),
75 ) {
76 keys.insert(format!("{rt}/{id}"));
77 }
78 }
79 keys
80}
81
82pub fn reference_type_id(reference: &str) -> Option<String> {
88 let without_query = reference.split(['?', '#']).next().unwrap_or(reference);
90 let trimmed = without_query.trim_end_matches('/');
91 let mut segments = trimmed.rsplitn(3, '/');
92 let id = segments.next()?;
93 let resource_type = segments.next()?;
94 if id.is_empty() || resource_type.is_empty() {
95 return None;
96 }
97 if !resource_type
99 .chars()
100 .next()
101 .map(|c| c.is_ascii_uppercase())
102 .unwrap_or(false)
103 {
104 return None;
105 }
106 Some(format!("{resource_type}/{id}"))
107}
108
109pub fn resolves_in_bundle(reference: &str, bundle_keys: &BTreeSet<String>) -> bool {
111 match reference_type_id(reference) {
112 Some(key) => bundle_keys.contains(&key),
113 None => false,
114 }
115}
116
117fn collect_into(value: &Value, out: &mut BTreeSet<String>) {
118 match value {
119 Value::Object(map) => {
120 if let Some(Value::String(reference)) = map.get("reference") {
121 out.insert(reference.clone());
122 }
123 for child in map.values() {
124 collect_into(child, out);
125 }
126 }
127 Value::Array(items) => {
128 for item in items {
129 collect_into(item, out);
130 }
131 }
132 _ => {}
133 }
134}
135
136fn bundle_to_json(bundle: &SofBundle) -> Option<Value> {
137 match bundle {
138 #[cfg(feature = "R4")]
139 SofBundle::R4(b) => serde_json::to_value(b).ok(),
140 #[cfg(feature = "R4B")]
141 SofBundle::R4B(b) => serde_json::to_value(b).ok(),
142 #[cfg(feature = "R5")]
143 SofBundle::R5(b) => serde_json::to_value(b).ok(),
144 #[cfg(feature = "R6")]
145 SofBundle::R6(b) => serde_json::to_value(b).ok(),
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn extracts_type_id_from_relative_and_absolute() {
155 assert_eq!(
156 reference_type_id("Patient/123").as_deref(),
157 Some("Patient/123")
158 );
159 assert_eq!(
160 reference_type_id("https://example.org/fhir/Patient/123").as_deref(),
161 Some("Patient/123")
162 );
163 assert_eq!(
164 reference_type_id("https://example.org/fhir/Patient/123?_format=json").as_deref(),
165 Some("Patient/123")
166 );
167 assert_eq!(reference_type_id("#contained"), None);
168 assert_eq!(reference_type_id("urn:uuid:abc"), None);
169 }
170
171 #[test]
172 fn collects_nested_references() {
173 let resource = serde_json::json!({
174 "resourceType": "Encounter",
175 "subject": { "reference": "Patient/1" },
176 "participant": [
177 { "individual": { "reference": "Practitioner/2" } },
178 { "individual": { "reference": "Practitioner/3" } }
179 ]
180 });
181 let mut refs = collect_references_from_json(&resource);
182 refs.sort();
183 assert_eq!(refs, vec!["Patient/1", "Practitioner/2", "Practitioner/3"]);
184 }
185}