1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
// SPDX-FileCopyrightText: Cadasto B.V.
// SPDX-License-Identifier: BUSL-1.1
//! How a [`Document`] takes the report or the composition a mapping run
//! wrote (no specification governs the split: our own design).
use fhir_types::codec::Json;
use fhir_types::r4::bundle::Bundle;
use fhir_types::r4::composition::CompositionSection;
use fhir_types::r4::meta::Meta;
use fhir_types::r4::reference::Reference;
use fhir_types::r4::resource::Resource;
use crate::document::section::MappedSection;
use crate::document::{Document, DocumentError, LOINC, codec, text};
impl Document {
/// Adds the resources of `run`, the `Bundle` one mapping run answers,
/// with its one `DiagnosticReport` as the report the form holds beside
/// the composition, and returns the `fullUrl` of each result the report
/// names, in its order, the entries of the section that lists them.
///
/// The run's `OperationOutcome`, the losses it declared, reports the run
/// and stays out of the document; read them from the run itself.
/// Every other resource of the run, its `Provenance` included, is added
/// as [`Document::add`] adds the output of one run, and the report takes
/// the same renaming, so its references keep naming the run's results.
/// The report is then set as [`Document::with_report`] sets one.
///
/// # Errors
///
/// [`DocumentError::Report`] when the form holds no report or already
/// holds one, [`DocumentError::Unmapped`] when the run holds no
/// `DiagnosticReport` or more than one, and the errors of
/// [`Document::add`].
pub fn with_mapped_report(&mut self, run: &Bundle) -> Result<Vec<String>, DocumentError> {
if self.form.report.is_none() {
return Err(DocumentError::Report {
expected: "holds no",
});
}
if self.report.is_some() {
return Err(DocumentError::Report {
expected: "takes one",
});
}
// NOTE: FHIR R4 documents.html: every resource of a document is reached from its
// Composition; the run's OperationOutcome reports the run and is no document content.
let resources: Vec<Resource> = run
.entry
.iter()
.filter_map(|entry| entry.resource.clone())
.filter(|resource| !matches!(resource, Resource::OperationOutcome(_)))
.collect();
let found = resources
.iter()
.filter(|resource| matches!(resource, Resource::DiagnosticReport(_)))
.count();
if found != 1 {
return Err(DocumentError::Unmapped { found });
}
let first = self.entries.len();
self.add(&resources)?;
let position = self
.entries
.iter()
.skip(first)
.position(|(_, resource)| matches!(resource, Resource::DiagnosticReport(_)))
.and_then(|offset| first.checked_add(offset))
.ok_or(DocumentError::Unmapped { found: 0 })?;
let (_, Resource::DiagnosticReport(report)) = self.entries.remove(position) else {
return Err(DocumentError::Unmapped { found: 0 });
};
let results = report
.result
.iter()
.filter_map(|result| result.reference.as_ref())
.filter_map(|reference| reference.value.as_deref())
// NOTE: FHIR R4 Bundle references (bundle.html#references): a relative reference resolves
// against the entry's RESTful base, and one with a scheme is its own fullUrl.
.map(|reference| {
if reference.contains(':') {
reference.to_owned()
} else {
format!("{}/{reference}", self.base)
}
})
.collect();
self.report = Some(*report);
Ok(results)
}
}
impl Document {
/// Returns the `fullUrl` of each `ImagingStudy` the document holds, in
/// the order its entries were added: the studies a mapped imaging run
/// wrote, which the `imagingstudy` section of `CompositionEuImaging`
/// lists (HL7 Europe Imaging Report 1.0.0-ballot).
#[cfg(feature = "imaging")]
pub fn studies(&self) -> impl Iterator<Item = &str> {
self.entries
.iter()
.filter(|(_, resource)| matches!(resource, Resource::ImagingStudy(_)))
.map(|(full_url, _)| full_url.as_str())
}
/// Adds the resources of `run`, the `Bundle` one mapping run answers,
/// whose context wrote the document's `Composition`, and returns each
/// section that composition holds, placed in the form by its code.
///
/// The run's `Composition` stays out of the document, which writes its
/// own from the form ([`Document::into_bundle`]); what the document
/// takes from it is the encounter it names and its sections. Every other
/// resource of the run, its `Provenance` included, is added as
/// [`Document::add`] adds the output of one run, and its
/// `OperationOutcome`, the losses the run declared, stays out as
/// [`Document::with_mapped_report`] leaves it. When the form takes an
/// encounter, the one the composition names becomes the document's and
/// claims the form's `Encounter` profile, as [`Document::with_encounter`]
/// makes it. Each section is placed in the form's slot whose LOINC code
/// a coding of the section's `code` carries, and its entries name the
/// `fullUrl`s the run's resources were added under.
///
/// # Errors
///
/// [`DocumentError::Uncomposed`] when the run holds no `Composition` or
/// more than one, [`DocumentError::Unslotted`] for a section whose code
/// names no slot of the form, [`DocumentError::Dangling`] for an
/// encounter or an entry the run does not hold, and the errors of
/// [`Document::add`].
pub fn with_mapped_composition(
&mut self,
run: &Bundle,
) -> Result<Vec<MappedSection>, DocumentError> {
let mut compositions = Vec::new();
let mut resources = Vec::new();
for resource in run.entry.iter().filter_map(|entry| entry.resource.clone()) {
match resource {
Resource::Composition(composition) => compositions.push(composition),
// NOTE: FHIR R4 documents.html: every resource of a document is reached from its
// Composition; the run's OperationOutcome reports the run and is no document content.
Resource::OperationOutcome(_) => {}
other => resources.push(other),
}
}
let [composition] = compositions.as_slice() else {
return Err(DocumentError::Uncomposed {
found: compositions.len(),
});
};
let keys = resources
.iter()
.map(key_of)
.collect::<Result<Vec<_>, DocumentError>>()?;
let urls = self.add(&resources)?;
let added = |reference: &Reference| -> Option<String> {
let named = reference.reference.as_ref()?.value.as_deref()?;
keys.iter()
.zip(&urls)
.find(|(key, url)| key.as_deref() == Some(named) || url.as_str() == named)
.map(|(_, url)| url.clone())
};
if let Some(profile) = self.form.encounter
&& let Some(named) = composition.encounter.as_ref()
{
let url = added(named).ok_or(DocumentError::Dangling { slice: "encounter" })?;
self.claim_encounter(&url, profile)?;
}
let mut sections = Vec::with_capacity(composition.section.len());
for section in &composition.section {
let codes = loinc_codes(section);
let slot = self
.form
.slots
.iter()
.find(|slot| codes.contains(&slot.code))
.ok_or_else(|| DocumentError::Unslotted {
codes: codes.iter().map(|code| (*code).to_owned()).collect(),
})?;
let entries = section
.entry
.iter()
.map(|entry| added(entry).ok_or(DocumentError::Dangling { slice: slot.slice }))
.collect::<Result<Vec<_>, _>>()?;
sections.push(MappedSection {
slot,
entries,
empty_reason: section.empty_reason.clone(),
});
}
Ok(sections)
}
/// Adds the entries the section of `run`'s `Composition` in the form's
/// slot `slice` names, the `Bundle` one mapping run answers, and returns
/// that section, or `None` when the composition holds no section in the
/// slot.
///
/// A document assembled one section at a time, from runs over the
/// compositions each section selects, takes from each run the section it
/// assembles alone: the entries that section names, and each `Provenance`
/// of the run narrowed to the targets among them, so no resource another
/// section of the run names enters the document. The run's `Composition`
/// and its `OperationOutcome` stay out, as
/// [`Document::with_mapped_composition`] leaves them.
///
/// # Errors
///
/// [`DocumentError::Unslotted`] when the form has no slot `slice`,
/// [`DocumentError::Uncomposed`] when the run holds no `Composition` or
/// more than one, [`DocumentError::Dangling`] for an entry the run does
/// not hold, and the errors of [`Document::add`].
pub fn with_mapped_section(
&mut self,
run: &Bundle,
slice: &str,
) -> Result<Option<MappedSection>, DocumentError> {
let slot = self
.form
.slots
.iter()
.find(|slot| slot.slice == slice)
.ok_or_else(|| DocumentError::Unslotted {
codes: vec![slice.to_owned()],
})?;
let mut compositions = Vec::new();
let mut keyed = Vec::new();
let mut provenances = Vec::new();
for resource in run.entry.iter().filter_map(|entry| entry.resource.clone()) {
match resource {
Resource::Composition(composition) => compositions.push(composition),
Resource::OperationOutcome(_) => {}
Resource::Provenance(provenance) => provenances.push(provenance),
other => keyed.push((key_of(&other)?, other)),
}
}
let [composition] = compositions.as_slice() else {
return Err(DocumentError::Uncomposed {
found: compositions.len(),
});
};
let Some(section) = composition
.section
.iter()
.find(|section| loinc_codes(section).contains(&slot.code))
else {
return Ok(None);
};
let mut named = Vec::with_capacity(section.entry.len());
for entry in §ion.entry {
let reference = entry
.reference
.as_ref()
.and_then(|reference| reference.value.as_deref())
.ok_or(DocumentError::Dangling { slice: slot.slice })?;
let (_, resource) = keyed
.iter()
.find(|(key, _)| key.as_deref() == Some(reference))
.ok_or(DocumentError::Dangling { slice: slot.slice })?;
named.push((reference.to_owned(), resource.clone()));
}
let mut resources: Vec<Resource> =
named.iter().map(|(_, resource)| resource.clone()).collect();
for mut provenance in provenances {
provenance.target.retain(|target| {
target
.reference
.as_ref()
.and_then(|reference| reference.value.as_deref())
.is_some_and(|reference| named.iter().any(|(key, _)| key == reference))
});
if !provenance.target.is_empty() {
resources.push(Resource::Provenance(provenance));
}
}
let urls = self.add(&resources)?;
Ok(Some(MappedSection {
slot,
entries: urls.into_iter().take(named.len()).collect(),
empty_reason: section.empty_reason.clone(),
}))
}
/// Sets the entry at `url`, an `Encounter`, as the encounter the
/// document is about, claiming `profile` unless it claims it already.
fn claim_encounter(&mut self, url: &str, profile: &str) -> Result<(), DocumentError> {
let Some((_, Resource::Encounter(encounter))) =
self.entries.iter_mut().find(|(held, _)| held == url)
else {
return Err(DocumentError::Dangling { slice: "encounter" });
};
let meta = encounter.meta.get_or_insert_with(Meta::default);
if !meta
.profile
.iter()
.any(|claimed| claimed.value.as_deref() == Some(profile))
{
meta.profile.push(profile.into());
}
let id = encounter.id.clone().ok_or_else(|| DocumentError::NoId {
resource: String::from("Encounter"),
})?;
self.encounter = Some(format!("Encounter/{id}"));
Ok(())
}
}
/// Returns the `Type/id` key of `resource`, or `None` when it carries no id.
fn key_of(resource: &Resource) -> Result<Option<String>, DocumentError> {
let object = Json::to_json(resource).map_err(codec)?;
Ok(match (text(&object, "resourceType"), text(&object, "id")) {
(Some(kind), Some(id)) => Some(format!("{kind}/{id}")),
_ => None,
})
}
/// Returns the LOINC codes the `code` of `section` carries.
fn loinc_codes(section: &CompositionSection) -> Vec<&str> {
section
.code
.iter()
.flat_map(|code| &code.coding)
.filter(|coding| {
coding
.system
.as_ref()
.and_then(|system| system.value.as_deref())
== Some(LOINC)
})
.filter_map(|coding| coding.code.as_ref().and_then(|code| code.value.as_deref()))
.collect()
}