Skip to main content

fml_model/
print.rs

1//! StructureMap → FML.
2//!
3//! The output compiles back to the same StructureMap: simple copies and batch identity rules are
4//! written in their short forms, generated rule names are left out, documentation becomes
5//! comments. The text is then laid out by the formatter.
6
7use std::fmt::Write;
8
9use fml_syntax::{FormatOptions, format, quote_fhirpath_string};
10use serde_json::Value;
11
12use crate::compile::{
13    AUTO_VARIABLE, BATCH_UNNAMED_PREFIX, SOURCE_ELEMENT_EXTENSION, TARGET_ELEMENT_EXTENSION,
14    make_id, source_path,
15};
16use crate::model::{Group, Parameter, ParameterValue, Rule, Source, StructureMap, Target};
17
18/// Writes a StructureMap as FML.
19pub fn print(map: &StructureMap) -> String {
20    let mut printer = Printer { out: String::new() };
21    printer.map(map);
22    let text = printer.out;
23    format(&text, &FormatOptions::default()).unwrap_or(text)
24}
25
26struct Printer {
27    out: String,
28}
29
30impl Printer {
31    fn line(&mut self, depth: usize, text: &str) {
32        for _ in 0..depth {
33            self.out.push_str("  ");
34        }
35        self.out.push_str(text);
36        self.out.push('\n');
37    }
38
39    /// Documentation as comments, written so the compiler reads back the same text. The compiler
40    /// trims each comment and joins the comments before a statement with `\r\n`, so text made of
41    /// trimmed single lines becomes `//` lines and anything else one `/* */` block.
42    fn documentation(&mut self, depth: usize, documentation: Option<&str>) {
43        let Some(documentation) = documentation.filter(|d| !d.is_empty()) else {
44            return;
45        };
46        let as_lines = documentation
47            .split("\r\n")
48            .all(|part| !part.contains(['\n', '\r']) && part == part.trim());
49        if as_lines {
50            for part in documentation.split("\r\n") {
51                self.line(depth, format!("// {part}").trim_end());
52            }
53        } else if !documentation.contains("*/") && documentation == documentation.trim() {
54            self.line(depth, &format!("/* {documentation} */"));
55        } else {
56            for line in documentation.lines() {
57                self.line(depth, format!("// {}", line.trim()).trim_end());
58            }
59        }
60    }
61
62    fn map(&mut self, map: &StructureMap) {
63        let metadata = |key: &str, value: &Option<String>| {
64            value
65                .as_ref()
66                .map(|v| format!("/// {key} = {}", quote_fhirpath_string(v)))
67        };
68        let mut header: Vec<String> = Vec::new();
69        header.extend(metadata("url", &map.url));
70        header.extend(metadata("version", &map.version));
71        header.extend(metadata("name", &map.name));
72        header.extend(metadata("title", &map.title));
73        header.extend(metadata("status", &map.status));
74        if let Some(description) = &map.description
75            && map.title.as_ref() != Some(description)
76        {
77            header.push(
78                if description.contains(['\n', '\r'])
79                    && !description.contains("\"\"\"")
80                    && !description.ends_with('"')
81                {
82                    format!("/// description = \"\"\"{description}\"\"\"")
83                } else {
84                    format!("/// description = {}", quote_fhirpath_string(description))
85                },
86            );
87        }
88        if let Some(experimental) = map.experimental {
89            header.push(format!("/// experimental = {experimental}"));
90        }
91        header.extend(metadata("date", &map.date));
92        header.extend(metadata("publisher", &map.publisher));
93        header.extend(metadata("purpose", &map.purpose));
94        header.extend(metadata("copyright", &map.copyright));
95        header.extend(crate::metadata::print(&map.other));
96        for line in header {
97            self.line(0, &line);
98        }
99        self.out.push('\n');
100
101        for contained in &map.contained {
102            if contained.get("resourceType").and_then(Value::as_str) == Some("ConceptMap") {
103                self.concept_map(contained);
104            }
105        }
106        for structure in &map.structure {
107            self.documentation(0, structure.documentation.as_deref());
108            let alias = structure
109                .alias
110                .as_ref()
111                .map(|a| format!(" alias {}", name(a)))
112                .unwrap_or_default();
113            self.line(
114                0,
115                &format!(
116                    "uses {}{alias} as {}",
117                    double_quoted(&structure.url),
118                    structure.mode
119                ),
120            );
121        }
122        self.out.push('\n');
123        for import in &map.import {
124            self.line(0, &format!("imports {}", double_quoted(import)));
125        }
126        self.out.push('\n');
127        for constant in &map.constants {
128            self.line(
129                0,
130                &format!("let {} = {};", name(&constant.name), constant.value),
131            );
132        }
133        self.out.push('\n');
134        for group in &map.group {
135            self.group(group);
136            self.out.push('\n');
137        }
138    }
139
140    fn concept_map(&mut self, concept_map: &Value) {
141        let id = concept_map.get("id").and_then(Value::as_str).unwrap_or("");
142        self.line(0, &format!("conceptmap {} {{", double_quoted(id)));
143        let groups: Vec<&Value> = concept_map
144            .get("group")
145            .and_then(Value::as_array)
146            .map(|g| g.iter().collect())
147            .unwrap_or_default();
148        let text = |value: &Value, key: &str| -> String {
149            value
150                .get(key)
151                .and_then(Value::as_str)
152                .unwrap_or("")
153                .to_owned()
154        };
155        // One prefix per system, in order of appearance: s, t, u, …
156        let mut prefixes: Vec<(String, char)> = Vec::new();
157        let prefix_for = |system: String, prefixes: &mut Vec<(String, char)>| -> char {
158            if let Some((_, p)) = prefixes.iter().find(|(s, _)| *s == system) {
159                return *p;
160            }
161            let next = (b's' + prefixes.len() as u8) as char;
162            prefixes.push((system, next));
163            next
164        };
165        let mut lines = Vec::new();
166        for group in &groups {
167            let source = prefix_for(text(group, "source"), &mut prefixes);
168            if let Some(mode) = group
169                .get("unmapped")
170                .and_then(|u| u.get("mode"))
171                .and_then(Value::as_str)
172            {
173                let mode = if mode == "use-source-code" {
174                    "provided"
175                } else {
176                    mode
177                };
178                lines.push(format!("unmapped for {source} = {mode}"));
179            }
180            let Some(target_system) = group.get("target").and_then(Value::as_str) else {
181                continue;
182            };
183            let target = prefix_for(target_system.to_owned(), &mut prefixes);
184            for element in group
185                .get("element")
186                .and_then(Value::as_array)
187                .into_iter()
188                .flatten()
189            {
190                let code = text(element, "code");
191                for mapping in element
192                    .get("target")
193                    .and_then(Value::as_array)
194                    .into_iter()
195                    .flatten()
196                {
197                    let relation = match mapping
198                        .get("relationship")
199                        .or_else(|| mapping.get("equivalence"))
200                        .and_then(Value::as_str)
201                        .unwrap_or("related-to")
202                    {
203                        "equivalent" | "equal" => "==",
204                        "not-related-to" | "disjoint" | "unmatched" => "!=",
205                        "source-is-narrower-than-target" | "narrower" | "specializes" => "<=",
206                        "source-is-broader-than-target" | "wider" | "subsumes" => ">=",
207                        _ => "-",
208                    };
209                    lines.push(format!(
210                        "{source}:{} {relation} {target}:{}",
211                        code_text(&code),
212                        code_text(&text(mapping, "code"))
213                    ));
214                }
215            }
216        }
217        for (system, prefix) in &prefixes {
218            self.line(1, &format!("prefix {prefix} = {}", double_quoted(system)));
219        }
220        self.out.push('\n');
221        for line in lines {
222            self.line(1, &line);
223        }
224        self.line(0, "}");
225        self.out.push('\n');
226    }
227
228    fn group(&mut self, group: &Group) {
229        self.documentation(0, group.documentation.as_deref());
230        let mut header = format!("group {}(", name(&group.name));
231        for (index, input) in group.input.iter().enumerate() {
232            if index > 0 {
233                header.push_str(", ");
234            }
235            let _ = write!(header, "{} {}", input.mode, name(&input.name));
236            if let Some(type_name) = &input.type_name {
237                let _ = write!(header, " : {type_name}");
238            }
239        }
240        header.push(')');
241        if let Some(extends) = &group.extends {
242            let _ = write!(header, " extends {}", name(extends));
243        }
244        match group.type_mode.as_deref() {
245            Some("types") => header.push_str(" <<types>>"),
246            Some("type-and-types") => header.push_str(" <<type+>>"),
247            _ => {}
248        }
249        header.push_str(" {");
250        self.line(0, &header);
251        self.rules(&group.rule, 1);
252        self.line(0, "}");
253    }
254
255    fn rules(&mut self, rules: &[Rule], depth: usize) {
256        let mut index = 0;
257        while index < rules.len() {
258            let end = batch_end(rules, index);
259            if end > index {
260                self.batch(&rules[index..=end], depth);
261                index = end + 1;
262            } else {
263                self.rule(&rules[index], depth);
264                index += 1;
265            }
266        }
267    }
268
269    /// `src -> tgt : a, b, c "prefix";`
270    fn batch(&mut self, rules: &[Rule], depth: usize) {
271        let first = &rules[0];
272        self.documentation(depth, first.documentation.as_deref());
273        let prefix = batch_prefix(first).unwrap_or_default();
274        let fields: Vec<String> = rules
275            .iter()
276            .map(|r| name(r.source[0].element.as_deref().unwrap_or("")))
277            .collect();
278        let rule_name = if prefix == BATCH_UNNAMED_PREFIX {
279            String::new()
280        } else {
281            format!(" {}", double_quoted(&prefix))
282        };
283        self.line(
284            depth,
285            &format!(
286                "{} -> {} : {}{rule_name};",
287                first.source[0].context,
288                first.target[0].context.as_deref().unwrap_or(""),
289                fields.join(", ")
290            ),
291        );
292    }
293
294    fn rule(&mut self, rule: &Rule, depth: usize) {
295        self.documentation(depth, rule.documentation.as_deref());
296        let abbreviate = is_abbreviated_copy(rule);
297        let sources: Vec<String> = rule
298            .source
299            .iter()
300            .map(|s| source_text(s, abbreviate))
301            .collect();
302        let mut text = sources.join(", ");
303        if !rule.target.is_empty() {
304            let targets: Vec<String> = rule
305                .target
306                .iter()
307                .map(|t| target_text(t, abbreviate))
308                .collect();
309            let _ = write!(text, " -> {}", targets.join(", "));
310        }
311        if !rule.dependent.is_empty() && !abbreviate {
312            let calls: Vec<String> = rule
313                .dependent
314                .iter()
315                .map(|d| {
316                    let arguments: Vec<String> = d.parameter.iter().map(parameter_text).collect();
317                    format!("{}({})", name(&d.name), arguments.join(", "))
318                })
319                .collect();
320            let _ = write!(text, " then {}", calls.join(", "));
321            if !rule.rule.is_empty() {
322                text.push_str(" {");
323            }
324        } else if !rule.rule.is_empty() {
325            text.push_str(" then {");
326        }
327        let name_suffix = match &rule.name {
328            Some(rule_name) if Some(rule_name) != generated_name(rule).as_ref() => {
329                format!(" {}", double_quoted(rule_name))
330            }
331            _ => String::new(),
332        };
333        if rule.rule.is_empty() {
334            self.line(depth, &format!("{text}{name_suffix};"));
335        } else {
336            self.line(depth, &text);
337            self.rules(&rule.rule, depth + 1);
338            self.line(depth, &format!("}}{name_suffix};"));
339        }
340    }
341}
342
343/// The name the compiler generates for an unnamed rule, which the printer can leave out.
344fn generated_name(rule: &Rule) -> Option<String> {
345    let [source] = rule.source.as_slice() else {
346        return None;
347    };
348    let path = source_path(source);
349    if path.is_empty() {
350        return None;
351    }
352    Some(match &source.type_name {
353        Some(type_name) => {
354            let mut chars = type_name.chars();
355            let capitalized: String = chars
356                .next()
357                .map(|c| c.to_uppercase().chain(chars).collect())
358                .unwrap_or_default();
359            path + &capitalized
360        }
361        None => path,
362    })
363}
364
365/// The dependent group older reference compilers (before 2025) added to every simple copy.
366const LEGACY_DEFAULT_GROUP: &str = "DefaultMappingGroupAnonymousAlias";
367
368/// Whether the rule's dependents are only the legacy implicit default-group call.
369fn has_only_legacy_default_dependent(rule: &Rule) -> bool {
370    match rule.dependent.as_slice() {
371        [] => true,
372        [dependent] => {
373            dependent.name == LEGACY_DEFAULT_GROUP
374                && dependent
375                    .parameter
376                    .iter()
377                    .all(|p| matches!(&p.value, ParameterValue::Id(id) if id == AUTO_VARIABLE))
378        }
379        _ => false,
380    }
381}
382
383/// The simple copy form, written `src.a -> tgt.a`: both sides bound to the automatic variable and
384/// an argument-less `create`.
385fn is_abbreviated_copy(rule: &Rule) -> bool {
386    let ([source], [target]) = (rule.source.as_slice(), rule.target.as_slice()) else {
387        return false;
388    };
389    source.element.is_some()
390        && source.variable.as_deref() == Some(AUTO_VARIABLE)
391        && target.context.is_some()
392        && target.element.is_some()
393        && target.variable.as_deref() == Some(AUTO_VARIABLE)
394        && matches!(target.transform.as_deref(), None | Some("create"))
395        && target.parameter.is_empty()
396        && has_only_legacy_default_dependent(rule)
397        && rule.rule.is_empty()
398}
399
400/// A rule that a batch identity rule produces: `src.a -> tgt.a` with the automatic variable.
401fn is_identity(rule: &Rule) -> bool {
402    let ([source], [target]) = (rule.source.as_slice(), rule.target.as_slice()) else {
403        return false;
404    };
405    is_abbreviated_copy(rule)
406        && source.extension.is_empty()
407        && target.extension.is_empty()
408        && source.type_name.is_none()
409        && source.min.is_none()
410        && source.list_mode.is_none()
411        && source.default_value.is_none()
412        && source.condition.is_none()
413        && source.check.is_none()
414        && source.log_message.is_none()
415        && target.list_mode.is_empty()
416        && source.element == target.element
417}
418
419/// The batch name prefix of an identity rule named `prefix` + field.
420fn batch_prefix(rule: &Rule) -> Option<String> {
421    if !is_identity(rule) {
422        return None;
423    }
424    let suffix = make_id(rule.source[0].element.as_deref()?);
425    let name = rule.name.as_deref()?;
426    let prefix = name.strip_suffix(suffix.as_str())?;
427    (!suffix.is_empty() && !prefix.is_empty()).then(|| prefix.to_owned())
428}
429
430/// The last index of a batch identity run starting at `start`, or `start` when there is none.
431fn batch_end(rules: &[Rule], start: usize) -> usize {
432    let Some(prefix) = batch_prefix(&rules[start]) else {
433        return start;
434    };
435    let contexts = |rule: &Rule| {
436        (
437            rule.source[0].context.clone(),
438            rule.target[0].context.clone(),
439        )
440    };
441    let first = contexts(&rules[start]);
442    let mut end = start;
443    for (index, rule) in rules.iter().enumerate().skip(start + 1) {
444        if rule.documentation.is_some()
445            || batch_prefix(rule).as_deref() != Some(prefix.as_str())
446            || contexts(rule) != first
447        {
448            break;
449        }
450        end = index;
451    }
452    // A single rule is a batch only when its name could not come from the plain form.
453    if end == start && prefix != BATCH_UNNAMED_PREFIX {
454        return start;
455    }
456    end
457}
458
459fn source_text(source: &Source, abbreviate: bool) -> String {
460    let mut text = source.context.clone();
461    for segment in source.element.iter().chain(
462        source
463            .extension
464            .iter()
465            .filter(|e| e.url == SOURCE_ELEMENT_EXTENSION)
466            .filter_map(|e| e.value_string.as_ref()),
467    ) {
468        text.push('.');
469        text.push_str(&name(segment));
470    }
471    if let Some(type_name) = &source.type_name {
472        let _ = write!(text, " : {type_name}");
473    }
474    if let Some(min) = source.min {
475        let _ = write!(text, " {min}..{}", source.max.as_deref().unwrap_or("*"));
476    }
477    if let Some(mode) = &source.list_mode {
478        let _ = write!(text, " {mode}");
479    }
480    if let Some(default) = &source.default_value {
481        let _ = write!(text, " default ({default})");
482    }
483    if !abbreviate && let Some(variable) = &source.variable {
484        let _ = write!(text, " as {}", name(variable));
485    }
486    if let Some(condition) = &source.condition {
487        let _ = write!(text, " where ({condition})");
488    }
489    if let Some(check) = &source.check {
490        let _ = write!(text, " check ({check})");
491    }
492    if let Some(log) = &source.log_message {
493        let _ = write!(text, " log ({log})");
494    }
495    text
496}
497
498fn target_text(target: &Target, abbreviate: bool) -> String {
499    let mut text = String::new();
500    if let Some(context) = &target.context {
501        text.push_str(context);
502        for segment in target.element.iter().chain(
503            target
504                .extension
505                .iter()
506                .filter(|e| e.url == TARGET_ELEMENT_EXTENSION)
507                .filter_map(|e| e.value_string.as_ref()),
508        ) {
509            text.push('.');
510            text.push_str(&name(segment));
511        }
512    }
513    if !abbreviate && let Some(transform) = &target.transform {
514        if target.context.is_some() {
515            text.push_str(" = ");
516        }
517        let values: Vec<&ParameterValue> = target.parameter.iter().map(|p| &p.value).collect();
518        match (transform.as_str(), values.as_slice()) {
519            ("copy", [_]) => text.push_str(&parameter_text(&target.parameter[0])),
520            ("evaluate", [ParameterValue::String(expression)]) => {
521                let _ = write!(text, "({expression})");
522            }
523            ("evaluate", [_, ParameterValue::String(expression)]) => {
524                let _ = write!(
525                    text,
526                    "evaluate({}, {expression})",
527                    parameter_text(&target.parameter[0])
528                );
529            }
530            (transform, _) => {
531                let arguments: Vec<String> = target.parameter.iter().map(parameter_text).collect();
532                let _ = write!(text, "{transform}({})", arguments.join(", "));
533            }
534        }
535    }
536    if !abbreviate && let Some(variable) = &target.variable {
537        let _ = write!(text, " as {}", name(variable));
538    }
539    for mode in &target.list_mode {
540        let _ = write!(text, " {mode}");
541        if mode == "share"
542            && let Some(id) = &target.list_rule_id
543        {
544            let _ = write!(text, " {}", name(id));
545        }
546    }
547    text
548}
549
550fn parameter_text(parameter: &Parameter) -> String {
551    match &parameter.value {
552        ParameterValue::Id(id) => id.split('.').map(name).collect::<Vec<_>>().join("."),
553        ParameterValue::String(value) => quote_fhirpath_string(value),
554        ParameterValue::Boolean(value) => value.to_string(),
555        ParameterValue::Integer(value) => value.to_string(),
556        ParameterValue::Decimal(value) => value.to_string(),
557        ParameterValue::Date(value) | ParameterValue::DateTime(value) => format!("@{value}"),
558        ParameterValue::Time(value) => format!("@T{value}"),
559    }
560}
561
562/// An identifier, in backticks when it is not a plain name.
563fn name(text: &str) -> String {
564    let plain = text
565        .chars()
566        .next()
567        .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
568        && text.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
569    if plain {
570        text.to_owned()
571    } else {
572        format!("`{}`", text.replace('\\', "\\\\").replace('`', "\\`"))
573    }
574}
575
576fn code_text(code: &str) -> String {
577    let plain = !code.is_empty() && code.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
578    let starts_like_name = code
579        .chars()
580        .next()
581        .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
582    if plain && (starts_like_name || code.chars().all(|c| c.is_ascii_digit())) {
583        code.to_owned()
584    } else {
585        double_quoted(code)
586    }
587}
588
589fn double_quoted(text: &str) -> String {
590    format!("\"{}\"", text.replace('\\', "\\\\").replace('"', "\\\""))
591}