ferrobridge_term/concept.rs
1// SPDX-FileCopyrightText: Cadasto B.V.
2// SPDX-License-Identifier: BUSL-1.1
3
4//! The values the three operations answer with, in one shape for every wire
5//! version.
6//!
7//! R4 and R4B declare the same `out` parameters for `CodeSystem/$lookup`,
8//! `ConceptMap/$translate` and `ValueSet/$validate-code`. STU3 declares a
9//! subset of them: its `property.value` is a `code`, and its
10//! `$validate-code` has no `issues`. One set of types carries all three, and
11//! the release decides which generated module reads the `Parameters`.
12
13/// A code in a code system, the neutral form of a FHIR `Coding`
14/// (<https://hl7.org/fhir/R4/datatypes.html#Coding>).
15#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub struct Concept {
17 /// `Coding.system`.
18 pub system: Option<String>,
19 /// `Coding.version`.
20 pub version: Option<String>,
21 /// `Coding.code`.
22 pub code: Option<String>,
23 /// `Coding.display`.
24 pub display: Option<String>,
25}
26
27/// One alternative representation of a concept, as the `designation` out
28/// parameter of `CodeSystem/$lookup` carries it.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct Designation {
31 /// The language this designation is defined for.
32 pub language: Option<String>,
33 /// How the designation is used.
34 pub usage: Option<Concept>,
35 /// The text value.
36 pub value: String,
37}
38
39/// The value of one property, kept as the FHIR choice member that carried it.
40///
41/// `CodeSystem/$lookup` types `property.value` as `Element`
42/// (`OperationDefinition-CodeSystem-lookup`, R4), so any FHIR type may arrive.
43/// Keeping the member name beside the lexical JSON loses nothing, and the
44/// lexical form is what preserves a decimal exactly
45/// (<https://hl7.org/fhir/R4/datatypes.html#decimal>).
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct PropertyValue {
48 /// The `value[x]` member name, for example `valueCode` or `valueCoding`.
49 pub member: String,
50 /// The lexical JSON the member carried.
51 pub value: fhir_types::codec::Value,
52}
53
54impl PropertyValue {
55 /// Returns the value as text when the member carried a JSON string.
56 ///
57 /// Every FHIR primitive whose JSON form is a string reads through this:
58 /// `code`, `string`, `dateTime` and the rest. A `valueCoding` or a numeric
59 /// member answers `None`, and the caller reads [`PropertyValue::value`].
60 #[must_use]
61 pub fn as_str(&self) -> Option<&str> {
62 match &self.value {
63 fhir_types::codec::Value::String(text) => Some(text.as_str()),
64 _ => None,
65 }
66 }
67}
68
69/// One property of a concept, as the `property` out parameter of
70/// `CodeSystem/$lookup` carries it.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct Property {
73 /// The property code.
74 pub code: String,
75 /// The value, when the server sent one.
76 pub value: Option<PropertyValue>,
77 /// The human-readable rendering of the value.
78 pub description: Option<String>,
79}
80
81/// How a translation match relates to the code that was translated.
82///
83/// The codes are the `ConceptMapEquivalence` value set
84/// (<https://hl7.org/fhir/R4/valueset-concept-map-equivalence.html>), which
85/// the `match.equivalence` documentation names; STU3 defines the same ten
86/// codes in the same hierarchy
87/// (<https://hl7.org/fhir/STU3/valueset-concept-map-equivalence.html>). The
88/// parameter declares no
89/// binding, so a code outside the set is kept as [`Equivalence::Other`] rather
90/// than refused; it can never be accepted, because [`Equivalence::accepted`]
91/// answers `true` for `equivalent` and `equal` alone.
92#[derive(Debug, Clone, PartialEq, Eq)]
93#[non_exhaustive]
94pub enum Equivalence {
95 /// `relatedto`: the concepts are related but the exact relationship is
96 /// unknown.
97 RelatedTo,
98 /// `equivalent`: the definitions are the same in meaning.
99 Equivalent,
100 /// `equal`: the definitions are exactly the same.
101 Equal,
102 /// `wider`: the target is broader in meaning.
103 Wider,
104 /// `subsumes`: the target subsumes the source.
105 Subsumes,
106 /// `narrower`: the target is narrower in meaning.
107 Narrower,
108 /// `specializes`: the target specializes the source.
109 Specializes,
110 /// `inexact`: the definitions overlap without either subsuming the other.
111 Inexact,
112 /// `unmatched`: no translation exists.
113 Unmatched,
114 /// `disjoint`: the source is mapped away from this target.
115 Disjoint,
116 /// A code the value set does not define, kept as the server sent it.
117 Other(String),
118}
119
120impl Equivalence {
121 /// Returns the equivalence `code` names.
122 #[must_use]
123 pub fn new(code: &str) -> Self {
124 match code {
125 "relatedto" => Self::RelatedTo,
126 "equivalent" => Self::Equivalent,
127 "equal" => Self::Equal,
128 "wider" => Self::Wider,
129 "subsumes" => Self::Subsumes,
130 "narrower" => Self::Narrower,
131 "specializes" => Self::Specializes,
132 "inexact" => Self::Inexact,
133 "unmatched" => Self::Unmatched,
134 "disjoint" => Self::Disjoint,
135 other => Self::Other(other.to_owned()),
136 }
137 }
138
139 /// Returns the code as the value set spells it.
140 #[must_use]
141 pub fn as_str(&self) -> &str {
142 match self {
143 Self::RelatedTo => "relatedto",
144 Self::Equivalent => "equivalent",
145 Self::Equal => "equal",
146 Self::Wider => "wider",
147 Self::Subsumes => "subsumes",
148 Self::Narrower => "narrower",
149 Self::Specializes => "specializes",
150 Self::Inexact => "inexact",
151 Self::Unmatched => "unmatched",
152 Self::Disjoint => "disjoint",
153 Self::Other(code) => code.as_str(),
154 }
155 }
156
157 /// Returns whether this equivalence makes the match a translation.
158 ///
159 /// Only `equivalent` and `equal` state that the target means what the
160 /// source means; `wider`, `narrower` and `inexact` state that it does not,
161 /// and writing one of those into a target system would assert a clinical
162 /// fact the map never made.
163 #[must_use]
164 pub fn accepted(&self) -> bool {
165 matches!(self, Self::Equivalent | Self::Equal)
166 }
167}
168
169/// One match of `ConceptMap/$translate`.
170///
171/// The operation "returns a set of parameters including a `result` for whether
172/// there is an acceptable match, and a list of possible matches. Note that the
173/// list of matches may include notes of codes for which mapping is
174/// specifically excluded, so implementers have to check the
175/// `match.equivalence` for each match"
176/// (`OperationDefinition-ConceptMap-translate`, R4), which is why every match
177/// is returned and [`Equivalence::accepted`] decides.
178#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct Match {
180 /// `match.equivalence`, when the server sent one.
181 pub equivalence: Option<Equivalence>,
182 /// `match.concept`, the concept that was mapped to.
183 pub concept: Option<Concept>,
184 /// `match.source`, the concept map that produced this match.
185 pub source: Option<String>,
186}
187
188impl Match {
189 /// Returns whether this match is a translation.
190 ///
191 /// A match with no equivalence is not one: the specification asks the
192 /// caller to check `match.equivalence`, and an absent value states
193 /// nothing.
194 #[must_use]
195 pub fn accepted(&self) -> bool {
196 self.equivalence.as_ref().is_some_and(Equivalence::accepted)
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::{Concept, Equivalence, Match, PropertyValue};
203
204 #[test]
205 fn only_equivalent_and_equal_are_accepted() {
206 for code in ["equivalent", "equal"] {
207 assert!(
208 Equivalence::new(code).accepted(),
209 "{code} should be a translation"
210 );
211 }
212 for code in [
213 "relatedto",
214 "wider",
215 "subsumes",
216 "narrower",
217 "specializes",
218 "inexact",
219 "unmatched",
220 "disjoint",
221 ] {
222 assert!(
223 !Equivalence::new(code).accepted(),
224 "{code} should not be a translation"
225 );
226 }
227 }
228
229 #[test]
230 fn an_unknown_equivalence_keeps_its_code_and_is_never_accepted() {
231 let equivalence = Equivalence::new("source-is-narrower-than-target");
232 assert_eq!(
233 Equivalence::Other("source-is-narrower-than-target".to_owned()),
234 equivalence
235 );
236 assert_eq!("source-is-narrower-than-target", equivalence.as_str());
237 assert!(!equivalence.accepted());
238 }
239
240 #[test]
241 fn a_match_without_an_equivalence_is_not_a_translation() {
242 let unstated = Match {
243 equivalence: None,
244 concept: Some(Concept::default()),
245 source: None,
246 };
247 assert!(!unstated.accepted());
248 }
249
250 #[test]
251 fn a_string_property_value_reads_as_text() {
252 let text = PropertyValue {
253 member: "valueString".to_owned(),
254 value: fhir_types::codec::Value::String("a note".to_owned()),
255 };
256 assert_eq!(Some("a note"), text.as_str());
257 let flag = PropertyValue {
258 member: "valueBoolean".to_owned(),
259 value: fhir_types::codec::Value::Bool(true),
260 };
261 assert_eq!(None, flag.as_str());
262 }
263}