Skip to main content

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}