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
//! Converting a resource from one FHIR release to another, saying what was
//! lost.
//!
//! The releases do not share model types (spec 12, R12.4), and there is no
//! `From`/`Into` between them: a conversion that compiled silently would be a
//! conversion whose losses were invisible, and in a health record that is worse
//! than one that fails. What this module offers instead is an **explicit**
//! conversion of the wire form, which hands back a [`LossReport`] naming
//! everything it changed or discarded.
//!
//! ```
//! use fhir::convert;
//! use fhir::r5::R5;
//! use serde_json::json;
//!
//! let patient = json!({
//! "resourceType": "Patient",
//! "id": "example",
//! "active": true,
//! });
//!
//! // Converting a release to itself is the degenerate case, and it is lossless.
//! let out = convert::between::<R5, R5>(&patient);
//! assert!(out.report.is_lossless(), "{}", out.report);
//! assert_eq!(out.value, patient);
//! ```
//!
//! Across releases it is not lossless, and the report is how you find out what
//! it cost. `Patient.animal` existed in R3 and was removed in R4:
//!
//! ```ignore
//! // Requires `--features "r3 r4"`; see tests/convert_releases.rs.
//! use fhir::{convert, r3::R3, r4::R4};
//! use serde_json::json;
//!
//! let r3_patient = json!({
//! "resourceType": "Patient",
//! "animal": { "species": { "text": "canine" } },
//! });
//!
//! let out = convert::between::<R3, R4>(&r3_patient);
//! assert!(out.value.get("animal").is_none());
//! assert!(out.report.iter().any(|l| l.path == "Patient.animal"));
//! ```
//!
//! # What it does and does not do
//!
//! It is a **structural** conversion, driven by both releases' generated
//! element tables. It knows what each release's elements are, which types a
//! `value[x]` admits, what repeats and what does not, and what each release
//! requires. It does not know that one release's element was *renamed* into
//! another's — that is a semantic remapping, and guessing at one would be
//! precisely the silent mangling the separate types exist to prevent. Renamed
//! elements are reported as [`LossKind::ElementRemoved`], which is the truthful
//! answer: this layer did not carry them over.
//!
//! Because it is driven by the tables rather than by hand-written rules, every
//! pair of modelled releases is convertible in both directions, including
//! through R2 and R6.
pub use ;
use Release;
use Value;
/// Convert a resource's JSON from release `S` to release `T`.
///
/// Name the releases by their marker types — [`r5::R5`](crate::r5::R5) and
/// friends — and the element tables come from them:
///
/// ```
/// use fhir::convert;
/// use fhir::r5::R5;
///
/// let obs = serde_json::json!({
/// "resourceType": "Observation",
/// "status": "final",
/// "code": { "text": "body weight" },
/// });
///
/// let out = convert::between::<R5, R5>(&obs);
/// assert!(out.report.is_lossless(), "{}", out.report);
/// ```
///
/// The returned [`Converted::value`] holds only what `T` accepts, so
/// deserializing it into `T`'s model succeeds where feeding it the source's
/// JSON directly would have failed — or, worse, quietly succeeded with fields
/// missing.
/// Convert from release `S` to release `T`, or refuse.
///
/// [`between`] always produces a document and tells you what it cost. This
/// produces one only when it cost nothing, which is the right shape for callers
/// who would rather reject a document than forward a lossy version of it — in a
/// clinical exchange, a dropped element is a dropped fact, and the receiver has
/// no way to know it was ever there.
///
/// ```
/// use fhir::convert;
/// use fhir::r5::R5;
///
/// let obs = serde_json::json!({
/// "resourceType": "Observation",
/// "status": "final",
/// "code": { "text": "body weight" },
/// });
///
/// let value = convert::strict::<R5, R5>(&obs).expect("a release can represent its own documents");
/// assert_eq!(value["status"], "final");
/// ```
///
/// The error carries the whole [`LossReport`], so a caller that rejects a
/// document can still say precisely why:
///
/// ```
/// # use fhir::convert;
/// # use fhir::r5::R5;
/// let not_a_resource = serde_json::json!({ "status": "final" });
///
/// match convert::strict::<R5, R5>(¬_a_resource) {
/// Ok(_) => panic!("it has no resourceType"),
/// Err(report) => assert!(!report.is_lossless()),
/// }
/// ```
///
/// # Errors
///
/// The [`LossReport`], whenever the conversion was not lossless. See
/// [`Converted::strict`] for why warnings count as well as discarded data.
/// Convert a typed resource from release `S` into release `T`'s JSON.
///
/// The same conversion as [`between`], starting from a value of the source
/// release's model rather than from JSON you already have.
///
/// It takes the release's [`Resource`](fhir_core::release::Release::Resource)
/// enum rather than a bare resource struct, and must: `resourceType` comes from
/// that enum's serde tag, so a `Patient` on its own does not say it is a
/// `Patient` and there would be nothing for the converter to key on.
///
/// ```
/// use fhir::convert;
/// use fhir::r5::{R5, resources::{Patient, Resource}, types};
///
/// let patient = Resource::Patient(Box::new(Patient {
/// id: Some(types::String("example".to_string())),
/// ..Default::default()
/// }));
///
/// let out = convert::from_typed::<R5, R5>(&patient);
/// assert_eq!(out.value["id"], "example");
/// assert!(out.report.is_lossless(), "{}", out.report);
/// ```