Skip to main content

ferrobridge_term/
outcome.rs

1// SPDX-FileCopyrightText: Cadasto B.V.
2// SPDX-License-Identifier: BUSL-1.1
3
4//! What each operation answers, as one enum per operation.
5//!
6//! A negative answer the operation states in its own `out` parameters is a
7//! variant here; an answer the server refused is a
8//! [`crate::error::Error`] carrying the upstream status and body. Nothing is
9//! ever flattened into an empty value.
10
11use crate::concept::{Concept, Designation, Match, Property};
12use crate::error::{Outcome, UpstreamError};
13
14/// What `CodeSystem/$lookup` answered.
15#[derive(Debug)]
16#[non_exhaustive]
17pub enum LookupOutcome {
18    /// The server knows the code and returned its details.
19    Found {
20        /// `name`, the code system's name. The operation declares it `1..1`.
21        name: String,
22        /// `version`, the code system version the answer is from.
23        version: Option<String>,
24        /// `display`, the preferred display. The operation declares it `1..1`.
25        display: String,
26        /// Every `designation` the server returned.
27        designations: Vec<Designation>,
28        /// Every `property` the server returned.
29        properties: Vec<Property>,
30    },
31    /// The server does not know the code, or does not know its system.
32    NotFound {
33        /// The upstream status and body, with any `tx-issue-type` coding.
34        outcome: Box<UpstreamError>,
35    },
36}
37
38/// What `ConceptMap/$translate` answered.
39#[derive(Debug)]
40#[non_exhaustive]
41pub enum TranslateOutcome {
42    /// `result` was `true`: the server found at least one match.
43    ///
44    /// Every match is carried with its equivalence, including the ones that
45    /// are not translations, because the specification asks the caller to
46    /// check `match.equivalence` for each one.
47    Translated {
48        /// The matches, in the order the server sent them.
49        matches: Vec<Match>,
50        /// `message`, when the server sent one.
51        message: Option<String>,
52    },
53    /// `result` was `false`: the server found no acceptable match.
54    NoMatch {
55        /// `message`, the server's error detail for a false `result`.
56        message: Option<String>,
57    },
58}
59
60impl TranslateOutcome {
61    /// Returns every match that is a translation.
62    ///
63    /// Only `equivalent` and `equal` matches are yielded
64    /// (<https://hl7.org/fhir/R4/valueset-concept-map-equivalence.html>), so a
65    /// `wider`, `narrower` or `inexact` match never reaches a target system as
66    /// a translation.
67    pub fn accepted(&self) -> impl Iterator<Item = &Match> {
68        let matches = match self {
69            Self::Translated { matches, .. } => matches.as_slice(),
70            Self::NoMatch { .. } => &[],
71        };
72        matches.iter().filter(|found| found.accepted())
73    }
74}
75
76/// What `ValueSet/$validate-code` answered.
77#[derive(Debug)]
78#[non_exhaustive]
79pub enum ValidateOutcome {
80    /// `result` was `true`: the code is a member of the value set.
81    Valid {
82        /// `display`, "a valid display for the concept if the system wishes to
83        /// display this to a user".
84        display: Option<String>,
85        /// `message`, which carries hints and warnings beside a true `result`.
86        message: Option<String>,
87    },
88    /// `result` was `false`: the code is not a member of the value set.
89    Invalid {
90        /// `message`, the server's error detail.
91        message: Option<String>,
92        /// `issues`, the itemised `OperationOutcome` the terminology ecosystem
93        /// asks for beside a false `result`
94        /// (<https://hl7.org/fhir/uv/tx-ecosystem/requirements.html>).
95        outcome: Option<Outcome>,
96    },
97}
98
99impl ValidateOutcome {
100    /// Returns whether the code is a member of the value set.
101    #[must_use]
102    pub fn is_valid(&self) -> bool {
103        matches!(self, Self::Valid { .. })
104    }
105}
106
107/// One operation to send, for a single call or as a batch entry.
108#[derive(Debug, Clone, PartialEq, Eq)]
109#[non_exhaustive]
110pub enum Request {
111    /// `CodeSystem/$lookup`.
112    Lookup {
113        /// The code system the code is in.
114        system: String,
115        /// The code to look up.
116        code: String,
117        /// The code system version, when the source data named one.
118        version: Option<String>,
119    },
120    /// `ConceptMap/$translate`.
121    Translate(Translation),
122    /// `ValueSet/$validate-code`.
123    ValidateCode(Validation),
124}
125
126impl Request {
127    /// Returns the operation, as `Resource/$code`.
128    #[must_use]
129    pub const fn operation(&self) -> &'static str {
130        match self {
131            Self::Lookup { .. } => crate::LOOKUP,
132            Self::Translate(_) => crate::TRANSLATE,
133            Self::ValidateCode(_) => crate::VALIDATE_CODE,
134        }
135    }
136}
137
138/// One `ConceptMap/$translate` question: what `code` from `system` maps to.
139///
140/// R4 declares `url`, `targetsystem` and `reverse` each `0..1`
141/// (<https://hl7.org/fhir/R4/conceptmap-operation-translate.html>), so each
142/// is sent only when it is set here.
143#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
144pub struct Translation {
145    /// `system`, the code system the code is in.
146    pub system: String,
147    /// `code`, the code to translate.
148    pub code: String,
149    /// `version`, the version of the system, if one was provided in the
150    /// source data.
151    pub version: Option<String>,
152    /// `url`, the canonical URL of the concept map to use, when one is named.
153    ///
154    /// STU3 `$translate` has no `url` parameter, so an STU3 client refuses a
155    /// translation that names one.
156    pub concept_map_url: Option<String>,
157    /// `targetsystem`, the code system to translate into, when one is named.
158    pub target_system: Option<String>,
159    /// `reverse`: "if this is true, then the operation should return all the
160    /// codes that might be mapped to this code".
161    pub reverse: Option<bool>,
162}
163
164impl Translation {
165    /// Returns the translation of `code` from `system`, naming no concept
166    /// map, no target system and no direction yet.
167    #[must_use]
168    pub fn new(system: impl Into<String>, code: impl Into<String>) -> Self {
169        Self {
170            system: system.into(),
171            code: code.into(),
172            version: None,
173            concept_map_url: None,
174            target_system: None,
175            reverse: None,
176        }
177    }
178
179    /// Names the version of the code system, sent as `version`.
180    #[must_use]
181    pub fn with_version(mut self, version: impl Into<String>) -> Self {
182        self.version = Some(version.into());
183        self
184    }
185
186    /// Names the concept map, sent as `url`.
187    #[must_use]
188    pub fn with_concept_map(mut self, url: impl Into<String>) -> Self {
189        self.concept_map_url = Some(url.into());
190        self
191    }
192
193    /// Names the code system to translate into, sent as `targetsystem`.
194    #[must_use]
195    pub fn with_target_system(mut self, system: impl Into<String>) -> Self {
196        self.target_system = Some(system.into());
197        self
198    }
199
200    /// Sets `reverse`, which asks for the codes that map to this one.
201    #[must_use]
202    pub const fn with_reverse(mut self, reverse: bool) -> Self {
203        self.reverse = Some(reverse);
204        self
205    }
206}
207
208/// One `ValueSet/$validate-code` question: is the coded value a member of
209/// the value set.
210#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
211pub struct Validation {
212    /// `url`, the canonical URL of the value set, without a version.
213    pub value_set_url: String,
214    /// `valueSetVersion`, the version of the value set, when the binding
215    /// names one.
216    ///
217    /// STU3 `$validate-code` has no such parameter, so an STU3 client
218    /// refuses a validation that names one.
219    pub value_set_version: Option<String>,
220    /// The coded value, in one of the three forms the operation accepts.
221    pub coded: Coded,
222}
223
224/// The coded value `ValueSet/$validate-code` tests, in the three input forms
225/// the operation declares
226/// (<https://hl7.org/fhir/R4/valueset-operation-validate-code.html>).
227#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
228#[non_exhaustive]
229pub enum Coded {
230    /// The `code`, `system`, `systemVersion` and `display` parameters. "If a
231    /// code is provided, a system or a context must be provided", so the
232    /// system is mandatory here.
233    Code {
234        /// `system`.
235        system: String,
236        /// `code`.
237        code: String,
238        /// `systemVersion`, the version of the system, if one was provided in
239        /// the source data. STU3 names this parameter `version`.
240        version: Option<String>,
241        /// `display`, the display associated with the code, if provided.
242        display: Option<String>,
243    },
244    /// The `coding` parameter.
245    Coding(Concept),
246    /// The `codeableConcept` parameter: "the server returns true if one of the
247    /// coding values is in the value set".
248    CodeableConcept {
249        /// `CodeableConcept.coding`.
250        coding: Vec<Concept>,
251        /// `CodeableConcept.text`.
252        text: Option<String>,
253    },
254}
255
256/// What one batch entry answered, in the shape of the request that produced
257/// it.
258#[derive(Debug)]
259#[non_exhaustive]
260pub enum BatchOutcome {
261    /// The answer to a [`Request::Lookup`].
262    Lookup(LookupOutcome),
263    /// The answer to a [`Request::Translate`].
264    Translate(TranslateOutcome),
265    /// The answer to a [`Request::ValidateCode`].
266    ValidateCode(ValidateOutcome),
267}
268
269#[cfg(test)]
270mod tests {
271    use super::{Request, TranslateOutcome, ValidateOutcome};
272    use crate::concept::{Concept, Equivalence, Match};
273
274    fn found(equivalence: &str, code: &str) -> Match {
275        Match {
276            equivalence: Some(Equivalence::new(equivalence)),
277            concept: Some(Concept {
278                system: Some("http://example.org/target".to_owned()),
279                code: Some(code.to_owned()),
280                ..Concept::default()
281            }),
282            source: None,
283        }
284    }
285
286    #[test]
287    fn accepted_yields_only_the_equivalent_and_equal_matches() {
288        let outcome = TranslateOutcome::Translated {
289            matches: vec![
290                found("wider", "broad"),
291                found("equivalent", "same"),
292                found("inexact", "close"),
293                found("equal", "identical"),
294            ],
295            message: None,
296        };
297        let accepted: Vec<Option<String>> = outcome
298            .accepted()
299            .map(|found| {
300                found
301                    .concept
302                    .as_ref()
303                    .and_then(|concept| concept.code.clone())
304            })
305            .collect();
306        assert_eq!(
307            vec![Some("same".to_owned()), Some("identical".to_owned())],
308            accepted
309        );
310    }
311
312    #[test]
313    fn a_no_match_outcome_accepts_nothing() {
314        let outcome = TranslateOutcome::NoMatch {
315            message: Some("no map".to_owned()),
316        };
317        assert_eq!(0, outcome.accepted().count());
318    }
319
320    #[test]
321    fn an_invalid_outcome_is_not_valid() {
322        let outcome = ValidateOutcome::Invalid {
323            message: Some("not in the value set".to_owned()),
324            outcome: None,
325        };
326        assert!(!outcome.is_valid());
327    }
328
329    #[test]
330    fn a_request_names_its_operation() {
331        let lookup = Request::Lookup {
332            system: "http://example.org/cs".to_owned(),
333            code: "a".to_owned(),
334            version: None,
335        };
336        assert_eq!("CodeSystem/$lookup", lookup.operation());
337    }
338}