1use std::collections::HashMap;
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::Arc;
7
8use serde_json::Value;
9
10pub type TranslateFuture<'a> =
12 Pin<Box<dyn Future<Output = Result<Option<Translation>, String>> + 'a>>;
13
14pub trait TranslateService {
18 fn translate<'a>(
21 &'a self,
22 url: &'a str,
23 system: Option<&'a str>,
24 code: &'a str,
25 ) -> TranslateFuture<'a>;
26}
27
28pub trait ConceptMapResolver {
30 fn resolve(&self, url: &str) -> Option<Arc<Value>>;
31}
32
33#[derive(Debug, Default, Clone, Copy)]
35pub struct NoConceptMaps;
36
37impl ConceptMapResolver for NoConceptMaps {
38 fn resolve(&self, _url: &str) -> Option<Arc<Value>> {
39 None
40 }
41}
42
43#[derive(Debug, Default, Clone)]
47pub struct ConceptMapSet {
48 by_url: HashMap<String, Arc<Value>>,
49}
50
51impl ConceptMapSet {
52 pub fn new() -> Self {
53 Self::default()
54 }
55
56 pub fn add(&mut self, concept_map: Value) -> bool {
58 if concept_map["resourceType"] != "ConceptMap" {
59 return false;
60 }
61 let concept_map = Arc::new(concept_map);
62 let mut keys = Vec::new();
63 if let Some(url) = concept_map["url"].as_str() {
64 keys.push(url.to_owned());
65 if let Some(version) = concept_map["version"].as_str() {
66 keys.push(format!("{url}|{version}"));
67 }
68 }
69 for artifact in concept_map["relatedArtifact"]
70 .as_array()
71 .into_iter()
72 .flatten()
73 {
74 if artifact["type"] == "predecessor"
75 && let Some(resource) = artifact["resource"].as_str()
76 {
77 keys.push(resource.to_owned());
78 }
79 }
80 for key in keys {
81 self.by_url
82 .entry(key)
83 .or_insert_with(|| concept_map.clone());
84 }
85 true
86 }
87
88 pub fn len(&self) -> usize {
89 self.by_url.len()
90 }
91
92 pub fn is_empty(&self) -> bool {
93 self.by_url.is_empty()
94 }
95}
96
97impl ConceptMapResolver for ConceptMapSet {
98 fn resolve(&self, url: &str) -> Option<Arc<Value>> {
99 self.by_url
100 .get(url)
101 .or_else(|| self.by_url.get(url.split('|').next().unwrap_or(url)))
102 .cloned()
103 }
104}
105
106#[derive(Debug, Clone, PartialEq)]
108pub struct Translation {
109 pub system: Option<String>,
110 pub code: Option<String>,
111 pub display: Option<String>,
112}
113
114pub fn translate(
122 concept_map: &Value,
123 system: Option<&str>,
124 code: &str,
125) -> Result<Option<Translation>, String> {
126 let base = |uri: &str| uri.split('|').next().unwrap_or(uri).to_owned();
127 for group in concept_map["group"].as_array().into_iter().flatten() {
128 if let (Some(system), Some(source)) = (system, group["source"].as_str())
129 && base(source) != base(system)
130 {
131 continue;
132 }
133 let target_system = group["target"].as_str().map(base);
134 if let Some(element) = group["element"]
135 .as_array()
136 .into_iter()
137 .flatten()
138 .find(|element| element["code"].as_str() == Some(code))
139 {
140 let accepted: Vec<&Value> = element["target"]
141 .as_array()
142 .into_iter()
143 .flatten()
144 .filter(|target| usable(target))
145 .collect();
146 return match accepted.as_slice() {
147 [] => Err(format!("`{code}` has no usable translation")),
148 [target] => Ok(Some(Translation {
149 system: target_system,
150 code: target["code"].as_str().map(str::to_owned),
151 display: target["display"].as_str().map(str::to_owned),
152 })),
153 _ => Err(format!("`{code}` has several translations")),
154 };
155 }
156 let unmapped = &group["unmapped"];
157 match unmapped["mode"].as_str() {
158 Some("use-source-code" | "provided") => {
159 return Ok(Some(Translation {
160 system: target_system,
161 code: Some(code.to_owned()),
162 display: None,
163 }));
164 }
165 Some("fixed") => {
166 return Ok(Some(Translation {
167 system: target_system,
168 code: unmapped["code"].as_str().map(str::to_owned),
169 display: unmapped["display"].as_str().map(str::to_owned),
170 }));
171 }
172 _ => {}
173 }
174 }
175 Ok(None)
176}
177
178fn usable(target: &Value) -> bool {
182 match (
183 target["relationship"].as_str(),
184 target["equivalence"].as_str(),
185 ) {
186 (Some(relationship), _) => matches!(
187 relationship,
188 "related-to" | "equivalent" | "source-is-narrower-than-target"
189 ),
190 (None, Some(equivalence)) => matches!(
191 equivalence,
192 "relatedto" | "inexact" | "equivalent" | "equal" | "wider" | "specializes"
193 ),
194 (None, None) => true,
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use serde_json::json;
201
202 use super::*;
203
204 fn concept_map() -> Value {
205 json!({
206 "resourceType": "ConceptMap",
207 "url": "http://example.org/ConceptMap/new",
208 "version": "1.0",
209 "relatedArtifact": [{ "type": "predecessor", "resource": "http://example.org/ConceptMap/old" }],
210 "group": [{
211 "source": "http://example.org/a|4.0.1",
212 "target": "http://example.org/b",
213 "element": [
214 { "code": "x", "target": [{ "code": "X", "display": "Ex", "relationship": "equivalent" }] },
215 { "code": "y", "target": [{ "code": "Y", "relationship": "not-related-to" }] },
216 { "code": "z", "target": [{ "code": "Z", "equivalence": "wider" }] }
217 ],
218 "unmapped": { "mode": "fixed", "code": "UNK" }
219 }]
220 })
221 }
222
223 #[test]
224 fn finds_maps_by_url_version_and_predecessor() {
225 let mut set = ConceptMapSet::new();
226 assert!(set.add(concept_map()));
227 assert!(!set.add(json!({ "resourceType": "ValueSet", "url": "http://example.org/vs" })));
228 for url in [
229 "http://example.org/ConceptMap/new",
230 "http://example.org/ConceptMap/new|1.0",
231 "http://example.org/ConceptMap/new|2.0",
232 "http://example.org/ConceptMap/old",
233 ] {
234 assert!(set.resolve(url).is_some(), "{url}");
235 }
236 }
237
238 #[test]
239 fn translates_codes() {
240 let map = concept_map();
241 let x = translate(&map, Some("http://example.org/a"), "x")
242 .unwrap()
243 .unwrap();
244 assert_eq!(x.code.as_deref(), Some("X"));
245 assert_eq!(x.system.as_deref(), Some("http://example.org/b"));
246 assert_eq!(x.display.as_deref(), Some("Ex"));
247 assert_eq!(
248 translate(&map, None, "z").unwrap().unwrap().code.as_deref(),
249 Some("Z")
250 );
251 assert!(translate(&map, None, "y").is_err());
253 assert_eq!(
255 translate(&map, None, "q").unwrap().unwrap().code.as_deref(),
256 Some("UNK")
257 );
258 assert_eq!(translate(&map, Some("http://other.org"), "x"), Ok(None));
259 }
260}