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