Skip to main content

fml_model/
flavor.rs

1//! StructureMap as FHIR R4 (and R4B) shape it, and back.
2//!
3//! The model follows R5 and the Mapping Language IG. R4 differs in a few elements, converted here
4//! the way the HL7 version converter (`StructureMap40_50`, `ConceptMap40_50`) does:
5//!
6//! | R5 / IG | R4 |
7//! |---|---|
8//! | `dependent.parameter` (`valueId`, …) | `dependent.variable` (strings) |
9//! | `source.defaultValue` (a FHIRPath literal) | `source.defaultValue[x]` (`'a'` → `defaultValueString`, `true` → `defaultValueBoolean`, numbers) |
10//! | target with a context | `contextType: variable` |
11//! | `group.typeMode` absent | `typeMode: none` (required in R4) |
12//! | `const` | not in R4: the R4 output has none, [`to_r4`] reports it |
13//! | parameter `valueDate`/`valueTime`/`valueDateTime` | `valueString` |
14//! | ConceptMap `relationship` | `equivalence` |
15//! | ConceptMap `unmapped.mode: use-source-code` | `provided` |
16
17use serde_json::{Map, Value, json};
18
19use crate::model::StructureMap;
20
21/// A StructureMap in R4 JSON, and the R5 content R4 cannot hold.
22#[derive(Debug, Clone, PartialEq)]
23pub struct R4Map {
24    pub json: Value,
25    /// What R4 cannot express, for the caller to report: `let` constants (left out) and default
26    /// values that are expressions rather than literals (kept as text in `defaultValueString`).
27    pub dropped: Vec<String>,
28}
29
30/// Converts a StructureMap to its R4 JSON form.
31pub fn to_r4(map: &StructureMap) -> R4Map {
32    let mut json = serde_json::to_value(map).expect("a StructureMap serializes");
33    let mut dropped = Vec::new();
34    let object = json.as_object_mut().expect("a StructureMap is an object");
35    if let Some(Value::Array(constants)) = object.remove("const") {
36        for constant in constants {
37            dropped.push(format!(
38                "const `{}`",
39                constant["name"].as_str().unwrap_or("?")
40            ));
41        }
42    }
43    for contained in array_mut(object, "contained") {
44        if contained["resourceType"] == "ConceptMap" {
45            concept_map_to_r4(contained);
46        }
47    }
48    for group in array_mut(object, "group") {
49        let group = group.as_object_mut().expect("a group is an object");
50        group
51            .entry("typeMode")
52            .or_insert_with(|| Value::String("none".into()));
53        for rule in array_mut(group, "rule") {
54            rule_to_r4(rule, &mut dropped);
55        }
56    }
57    R4Map { json, dropped }
58}
59
60/// Reads a StructureMap in R4 or R5 JSON. R4 elements are converted to the model; R5 content
61/// passes through.
62pub fn from_json(mut json: Value) -> Result<StructureMap, serde_json::Error> {
63    if let Some(object) = json.as_object_mut() {
64        for contained in array_mut(object, "contained") {
65            if contained["resourceType"] == "ConceptMap" {
66                concept_map_from_r4(contained);
67            }
68        }
69        for group in array_mut(object, "group") {
70            let group = group.as_object_mut().expect("a group is an object");
71            if group.get("typeMode").and_then(Value::as_str) == Some("none") {
72                group.remove("typeMode");
73            }
74            for rule in array_mut(group, "rule") {
75                rule_from_r4(rule);
76            }
77        }
78    }
79    serde_json::from_value(json)
80}
81
82fn array_mut<'a>(
83    object: &'a mut Map<String, Value>,
84    key: &str,
85) -> impl Iterator<Item = &'a mut Value> {
86    object
87        .get_mut(key)
88        .and_then(Value::as_array_mut)
89        .into_iter()
90        .flatten()
91}
92
93fn rule_to_r4(rule: &mut Value, dropped: &mut Vec<String>) {
94    let rule = rule.as_object_mut().expect("a rule is an object");
95    let name = rule
96        .get("name")
97        .and_then(Value::as_str)
98        .unwrap_or("?")
99        .to_owned();
100    for source in array_mut(rule, "source") {
101        let source = source.as_object_mut().expect("a source is an object");
102        if let Some(Value::String(default)) = source.remove("defaultValue") {
103            let (key, value) = literal_default(&default).unwrap_or_else(|| {
104                dropped.push(format!(
105                    "default `{default}` of rule `{name}` is not a literal"
106                ));
107                ("defaultValueString", Value::String(default.clone()))
108            });
109            source.insert(key.into(), value);
110        }
111    }
112    for target in array_mut(rule, "target") {
113        let target = target.as_object_mut().expect("a target is an object");
114        if target.contains_key("context") {
115            target.insert("contextType".into(), "variable".into());
116        }
117        for parameter in array_mut(target, "parameter") {
118            parameter_to_r4(parameter);
119        }
120    }
121    for dependent in array_mut(rule, "dependent") {
122        let dependent = dependent.as_object_mut().expect("a dependent is an object");
123        if let Some(Value::Array(parameters)) = dependent.remove("parameter") {
124            let variables: Vec<Value> = parameters
125                .iter()
126                .filter_map(|p| {
127                    p.as_object()?
128                        .iter()
129                        .find(|(k, _)| k.starts_with("value"))
130                        .map(|(_, v)| match v {
131                            Value::String(s) => Value::String(s.clone()),
132                            other => Value::String(other.to_string()),
133                        })
134                })
135                .collect();
136            dependent.insert("variable".into(), Value::Array(variables));
137        }
138    }
139    for nested in array_mut(rule, "rule") {
140        rule_to_r4(nested, dropped);
141    }
142}
143
144/// The R4 `defaultValue[x]` for a FHIRPath literal default.
145fn literal_default(expression: &str) -> Option<(&'static str, Value)> {
146    let text = expression.trim();
147    if text.len() >= 2 && text.starts_with('\'') && text.ends_with('\'') {
148        let inner = &text[1..text.len() - 1];
149        // A single string literal, not `'a' + 'b'`.
150        if !inner.replace("\\\\", "").replace("\\'", "").contains('\'') {
151            return Some(("defaultValueString", fml_syntax::ast::unescape(text).into()));
152        }
153        return None;
154    }
155    match text {
156        "true" => return Some(("defaultValueBoolean", true.into())),
157        "false" => return Some(("defaultValueBoolean", false.into())),
158        _ => {}
159    }
160    if let Ok(integer) = text.parse::<i64>() {
161        return Some(("defaultValueInteger", integer.into()));
162    }
163    if text.contains('.')
164        && let Ok(Value::Number(number)) = serde_json::from_str::<Value>(text)
165    {
166        return Some(("defaultValueDecimal", Value::Number(number)));
167    }
168    None
169}
170
171fn rule_from_r4(rule: &mut Value) {
172    let rule = rule.as_object_mut().expect("a rule is an object");
173    for source in array_mut(rule, "source") {
174        let source = source.as_object_mut().expect("a source is an object");
175        let key = source
176            .keys()
177            .find(|k| k.starts_with("defaultValue") && k.as_str() != "defaultValue")
178            .cloned();
179        if let Some(key) = key
180            && let Some(value) = source.remove(&key)
181        {
182            // R4 holds a value; R5 a FHIRPath expression: write the value as a literal.
183            let expression = match value {
184                Value::String(text) if key == "defaultValueBoolean" => text,
185                Value::String(text) => fml_syntax::quote_fhirpath_string(&text),
186                Value::Bool(flag) => flag.to_string(),
187                Value::Number(number) => number.to_string(),
188                other => other.to_string(),
189            };
190            source.insert("defaultValue".into(), Value::String(expression));
191        }
192    }
193    for target in array_mut(rule, "target") {
194        let target = target.as_object_mut().expect("a target is an object");
195        target.remove("contextType");
196    }
197    for dependent in array_mut(rule, "dependent") {
198        let dependent = dependent.as_object_mut().expect("a dependent is an object");
199        if let Some(Value::Array(variables)) = dependent.remove("variable") {
200            let parameters: Vec<Value> = variables
201                .into_iter()
202                .map(|v| json!({ "valueId": v }))
203                .collect();
204            dependent.insert("parameter".into(), Value::Array(parameters));
205        }
206    }
207    for nested in array_mut(rule, "rule") {
208        rule_from_r4(nested);
209    }
210}
211
212fn parameter_to_r4(parameter: &mut Value) {
213    let Some(object) = parameter.as_object_mut() else {
214        return;
215    };
216    for key in ["valueDate", "valueTime", "valueDateTime"] {
217        if let Some(value) = object.remove(key) {
218            object.insert("valueString".into(), value);
219        }
220    }
221}
222
223const RELATIONSHIPS: &[(&str, &str)] = &[
224    ("related-to", "relatedto"),
225    ("equivalent", "equivalent"),
226    ("source-is-narrower-than-target", "wider"),
227    ("source-is-broader-than-target", "narrower"),
228    ("not-related-to", "disjoint"),
229];
230
231fn concept_map_to_r4(concept_map: &mut Value) {
232    for group in concept_map["group"].as_array_mut().into_iter().flatten() {
233        if group["unmapped"]["mode"] == "use-source-code" {
234            group["unmapped"]["mode"] = "provided".into();
235        }
236        for element in group["element"].as_array_mut().into_iter().flatten() {
237            for target in element["target"].as_array_mut().into_iter().flatten() {
238                let Some(object) = target.as_object_mut() else {
239                    continue;
240                };
241                if let Some(Value::String(relationship)) = object.remove("relationship") {
242                    let equivalence = RELATIONSHIPS
243                        .iter()
244                        .find(|(r5, _)| *r5 == relationship)
245                        .map_or("relatedto", |(_, r4)| r4);
246                    object.insert("equivalence".into(), equivalence.into());
247                }
248            }
249        }
250    }
251}
252
253fn concept_map_from_r4(concept_map: &mut Value) {
254    for group in concept_map["group"].as_array_mut().into_iter().flatten() {
255        if group["unmapped"]["mode"] == "provided" {
256            group["unmapped"]["mode"] = "use-source-code".into();
257        }
258        for element in group["element"].as_array_mut().into_iter().flatten() {
259            for target in element["target"].as_array_mut().into_iter().flatten() {
260                let Some(object) = target.as_object_mut() else {
261                    continue;
262                };
263                if let Some(Value::String(equivalence)) = object.remove("equivalence") {
264                    let relationship = match equivalence.as_str() {
265                        "equal" => "equivalent",
266                        "subsumes" => "source-is-broader-than-target",
267                        "specializes" => "source-is-narrower-than-target",
268                        "unmatched" => "not-related-to",
269                        "inexact" => "related-to",
270                        other => RELATIONSHIPS
271                            .iter()
272                            .find(|(_, r4)| *r4 == other)
273                            .map_or("related-to", |(r5, _)| r5),
274                    };
275                    object.insert("relationship".into(), relationship.into());
276                }
277            }
278        }
279    }
280}