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}