1use 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
18pub 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 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 for line in header {
96 self.line(0, &line);
97 }
98 self.out.push('\n');
99
100 for contained in &map.contained {
101 if contained.get("resourceType").and_then(Value::as_str) == Some("ConceptMap") {
102 self.concept_map(contained);
103 }
104 }
105 for structure in &map.structure {
106 self.documentation(0, structure.documentation.as_deref());
107 let alias = structure
108 .alias
109 .as_ref()
110 .map(|a| format!(" alias {}", name(a)))
111 .unwrap_or_default();
112 self.line(
113 0,
114 &format!(
115 "uses {}{alias} as {}",
116 double_quoted(&structure.url),
117 structure.mode
118 ),
119 );
120 }
121 self.out.push('\n');
122 for import in &map.import {
123 self.line(0, &format!("imports {}", double_quoted(import)));
124 }
125 self.out.push('\n');
126 for constant in &map.constants {
127 self.line(
128 0,
129 &format!("let {} = {};", name(&constant.name), constant.value),
130 );
131 }
132 self.out.push('\n');
133 for group in &map.group {
134 self.group(group);
135 self.out.push('\n');
136 }
137 }
138
139 fn concept_map(&mut self, concept_map: &Value) {
140 let id = concept_map.get("id").and_then(Value::as_str).unwrap_or("");
141 self.line(0, &format!("conceptmap {} {{", double_quoted(id)));
142 let groups: Vec<&Value> = concept_map
143 .get("group")
144 .and_then(Value::as_array)
145 .map(|g| g.iter().collect())
146 .unwrap_or_default();
147 let text = |value: &Value, key: &str| -> String {
148 value
149 .get(key)
150 .and_then(Value::as_str)
151 .unwrap_or("")
152 .to_owned()
153 };
154 let mut prefixes: Vec<(String, char)> = Vec::new();
156 let prefix_for = |system: String, prefixes: &mut Vec<(String, char)>| -> char {
157 if let Some((_, p)) = prefixes.iter().find(|(s, _)| *s == system) {
158 return *p;
159 }
160 let next = (b's' + prefixes.len() as u8) as char;
161 prefixes.push((system, next));
162 next
163 };
164 let mut lines = Vec::new();
165 for group in &groups {
166 let source = prefix_for(text(group, "source"), &mut prefixes);
167 if let Some(mode) = group
168 .get("unmapped")
169 .and_then(|u| u.get("mode"))
170 .and_then(Value::as_str)
171 {
172 let mode = if mode == "use-source-code" {
173 "provided"
174 } else {
175 mode
176 };
177 lines.push(format!("unmapped for {source} = {mode}"));
178 }
179 let Some(target_system) = group.get("target").and_then(Value::as_str) else {
180 continue;
181 };
182 let target = prefix_for(target_system.to_owned(), &mut prefixes);
183 for element in group
184 .get("element")
185 .and_then(Value::as_array)
186 .into_iter()
187 .flatten()
188 {
189 let code = text(element, "code");
190 for mapping in element
191 .get("target")
192 .and_then(Value::as_array)
193 .into_iter()
194 .flatten()
195 {
196 let relation = match mapping
197 .get("relationship")
198 .or_else(|| mapping.get("equivalence"))
199 .and_then(Value::as_str)
200 .unwrap_or("related-to")
201 {
202 "equivalent" | "equal" => "==",
203 "not-related-to" | "disjoint" | "unmatched" => "!=",
204 "source-is-narrower-than-target" | "narrower" | "specializes" => "<=",
205 "source-is-broader-than-target" | "wider" | "subsumes" => ">=",
206 _ => "-",
207 };
208 lines.push(format!(
209 "{source}:{} {relation} {target}:{}",
210 code_text(&code),
211 code_text(&text(mapping, "code"))
212 ));
213 }
214 }
215 }
216 for (system, prefix) in &prefixes {
217 self.line(1, &format!("prefix {prefix} = {}", double_quoted(system)));
218 }
219 self.out.push('\n');
220 for line in lines {
221 self.line(1, &line);
222 }
223 self.line(0, "}");
224 self.out.push('\n');
225 }
226
227 fn group(&mut self, group: &Group) {
228 self.documentation(0, group.documentation.as_deref());
229 let mut header = format!("group {}(", name(&group.name));
230 for (index, input) in group.input.iter().enumerate() {
231 if index > 0 {
232 header.push_str(", ");
233 }
234 let _ = write!(header, "{} {}", input.mode, name(&input.name));
235 if let Some(type_name) = &input.type_name {
236 let _ = write!(header, " : {type_name}");
237 }
238 }
239 header.push(')');
240 if let Some(extends) = &group.extends {
241 let _ = write!(header, " extends {}", name(extends));
242 }
243 match group.type_mode.as_deref() {
244 Some("types") => header.push_str(" <<types>>"),
245 Some("type-and-types") => header.push_str(" <<type+>>"),
246 _ => {}
247 }
248 header.push_str(" {");
249 self.line(0, &header);
250 self.rules(&group.rule, 1);
251 self.line(0, "}");
252 }
253
254 fn rules(&mut self, rules: &[Rule], depth: usize) {
255 let mut index = 0;
256 while index < rules.len() {
257 let end = batch_end(rules, index);
258 if end > index {
259 self.batch(&rules[index..=end], depth);
260 index = end + 1;
261 } else {
262 self.rule(&rules[index], depth);
263 index += 1;
264 }
265 }
266 }
267
268 fn batch(&mut self, rules: &[Rule], depth: usize) {
270 let first = &rules[0];
271 self.documentation(depth, first.documentation.as_deref());
272 let prefix = batch_prefix(first).unwrap_or_default();
273 let fields: Vec<String> = rules
274 .iter()
275 .map(|r| name(r.source[0].element.as_deref().unwrap_or("")))
276 .collect();
277 let rule_name = if prefix == BATCH_UNNAMED_PREFIX {
278 String::new()
279 } else {
280 format!(" {}", double_quoted(&prefix))
281 };
282 self.line(
283 depth,
284 &format!(
285 "{} -> {} : {}{rule_name};",
286 first.source[0].context,
287 first.target[0].context.as_deref().unwrap_or(""),
288 fields.join(", ")
289 ),
290 );
291 }
292
293 fn rule(&mut self, rule: &Rule, depth: usize) {
294 self.documentation(depth, rule.documentation.as_deref());
295 let abbreviate = is_abbreviated_copy(rule);
296 let sources: Vec<String> = rule
297 .source
298 .iter()
299 .map(|s| source_text(s, abbreviate))
300 .collect();
301 let mut text = sources.join(", ");
302 if !rule.target.is_empty() {
303 let targets: Vec<String> = rule
304 .target
305 .iter()
306 .map(|t| target_text(t, abbreviate))
307 .collect();
308 let _ = write!(text, " -> {}", targets.join(", "));
309 }
310 if !rule.dependent.is_empty() && !abbreviate {
311 let calls: Vec<String> = rule
312 .dependent
313 .iter()
314 .map(|d| {
315 let arguments: Vec<String> = d.parameter.iter().map(parameter_text).collect();
316 format!("{}({})", name(&d.name), arguments.join(", "))
317 })
318 .collect();
319 let _ = write!(text, " then {}", calls.join(", "));
320 if !rule.rule.is_empty() {
321 text.push_str(" {");
322 }
323 } else if !rule.rule.is_empty() {
324 text.push_str(" then {");
325 }
326 let name_suffix = match &rule.name {
327 Some(rule_name) if Some(rule_name) != generated_name(rule).as_ref() => {
328 format!(" {}", double_quoted(rule_name))
329 }
330 _ => String::new(),
331 };
332 if rule.rule.is_empty() {
333 self.line(depth, &format!("{text}{name_suffix};"));
334 } else {
335 self.line(depth, &text);
336 self.rules(&rule.rule, depth + 1);
337 self.line(depth, &format!("}}{name_suffix};"));
338 }
339 }
340}
341
342fn generated_name(rule: &Rule) -> Option<String> {
344 let [source] = rule.source.as_slice() else {
345 return None;
346 };
347 let path = source_path(source);
348 if path.is_empty() {
349 return None;
350 }
351 Some(match &source.type_name {
352 Some(type_name) => {
353 let mut chars = type_name.chars();
354 let capitalized: String = chars
355 .next()
356 .map(|c| c.to_uppercase().chain(chars).collect())
357 .unwrap_or_default();
358 path + &capitalized
359 }
360 None => path,
361 })
362}
363
364const LEGACY_DEFAULT_GROUP: &str = "DefaultMappingGroupAnonymousAlias";
366
367fn has_only_legacy_default_dependent(rule: &Rule) -> bool {
369 match rule.dependent.as_slice() {
370 [] => true,
371 [dependent] => {
372 dependent.name == LEGACY_DEFAULT_GROUP
373 && dependent
374 .parameter
375 .iter()
376 .all(|p| matches!(&p.value, ParameterValue::Id(id) if id == AUTO_VARIABLE))
377 }
378 _ => false,
379 }
380}
381
382fn is_abbreviated_copy(rule: &Rule) -> bool {
385 let ([source], [target]) = (rule.source.as_slice(), rule.target.as_slice()) else {
386 return false;
387 };
388 source.element.is_some()
389 && source.variable.as_deref() == Some(AUTO_VARIABLE)
390 && target.context.is_some()
391 && target.element.is_some()
392 && target.variable.as_deref() == Some(AUTO_VARIABLE)
393 && matches!(target.transform.as_deref(), None | Some("create"))
394 && target.parameter.is_empty()
395 && has_only_legacy_default_dependent(rule)
396 && rule.rule.is_empty()
397}
398
399fn is_identity(rule: &Rule) -> bool {
401 let ([source], [target]) = (rule.source.as_slice(), rule.target.as_slice()) else {
402 return false;
403 };
404 is_abbreviated_copy(rule)
405 && source.extension.is_empty()
406 && target.extension.is_empty()
407 && source.type_name.is_none()
408 && source.min.is_none()
409 && source.list_mode.is_none()
410 && source.default_value.is_none()
411 && source.condition.is_none()
412 && source.check.is_none()
413 && source.log_message.is_none()
414 && target.list_mode.is_empty()
415 && source.element == target.element
416}
417
418fn batch_prefix(rule: &Rule) -> Option<String> {
420 if !is_identity(rule) {
421 return None;
422 }
423 let suffix = make_id(rule.source[0].element.as_deref()?);
424 let name = rule.name.as_deref()?;
425 let prefix = name.strip_suffix(suffix.as_str())?;
426 (!suffix.is_empty() && !prefix.is_empty()).then(|| prefix.to_owned())
427}
428
429fn batch_end(rules: &[Rule], start: usize) -> usize {
431 let Some(prefix) = batch_prefix(&rules[start]) else {
432 return start;
433 };
434 let contexts = |rule: &Rule| {
435 (
436 rule.source[0].context.clone(),
437 rule.target[0].context.clone(),
438 )
439 };
440 let first = contexts(&rules[start]);
441 let mut end = start;
442 for (index, rule) in rules.iter().enumerate().skip(start + 1) {
443 if rule.documentation.is_some()
444 || batch_prefix(rule).as_deref() != Some(prefix.as_str())
445 || contexts(rule) != first
446 {
447 break;
448 }
449 end = index;
450 }
451 if end == start && prefix != BATCH_UNNAMED_PREFIX {
453 return start;
454 }
455 end
456}
457
458fn source_text(source: &Source, abbreviate: bool) -> String {
459 let mut text = source.context.clone();
460 for segment in source.element.iter().chain(
461 source
462 .extension
463 .iter()
464 .filter(|e| e.url == SOURCE_ELEMENT_EXTENSION)
465 .filter_map(|e| e.value_string.as_ref()),
466 ) {
467 text.push('.');
468 text.push_str(&name(segment));
469 }
470 if let Some(type_name) = &source.type_name {
471 let _ = write!(text, " : {type_name}");
472 }
473 if let Some(min) = source.min {
474 let _ = write!(text, " {min}..{}", source.max.as_deref().unwrap_or("*"));
475 }
476 if let Some(mode) = &source.list_mode {
477 let _ = write!(text, " {mode}");
478 }
479 if let Some(default) = &source.default_value {
480 let _ = write!(text, " default ({default})");
481 }
482 if !abbreviate && let Some(variable) = &source.variable {
483 let _ = write!(text, " as {}", name(variable));
484 }
485 if let Some(condition) = &source.condition {
486 let _ = write!(text, " where ({condition})");
487 }
488 if let Some(check) = &source.check {
489 let _ = write!(text, " check ({check})");
490 }
491 if let Some(log) = &source.log_message {
492 let _ = write!(text, " log ({log})");
493 }
494 text
495}
496
497fn target_text(target: &Target, abbreviate: bool) -> String {
498 let mut text = String::new();
499 if let Some(context) = &target.context {
500 text.push_str(context);
501 for segment in target.element.iter().chain(
502 target
503 .extension
504 .iter()
505 .filter(|e| e.url == TARGET_ELEMENT_EXTENSION)
506 .filter_map(|e| e.value_string.as_ref()),
507 ) {
508 text.push('.');
509 text.push_str(&name(segment));
510 }
511 }
512 if !abbreviate && let Some(transform) = &target.transform {
513 if target.context.is_some() {
514 text.push_str(" = ");
515 }
516 let values: Vec<&ParameterValue> = target.parameter.iter().map(|p| &p.value).collect();
517 match (transform.as_str(), values.as_slice()) {
518 ("copy", [_]) => text.push_str(¶meter_text(&target.parameter[0])),
519 ("evaluate", [ParameterValue::String(expression)]) => {
520 let _ = write!(text, "({expression})");
521 }
522 ("evaluate", [_, ParameterValue::String(expression)]) => {
523 let _ = write!(
524 text,
525 "evaluate({}, {expression})",
526 parameter_text(&target.parameter[0])
527 );
528 }
529 (transform, _) => {
530 let arguments: Vec<String> = target.parameter.iter().map(parameter_text).collect();
531 let _ = write!(text, "{transform}({})", arguments.join(", "));
532 }
533 }
534 }
535 if !abbreviate && let Some(variable) = &target.variable {
536 let _ = write!(text, " as {}", name(variable));
537 }
538 for mode in &target.list_mode {
539 let _ = write!(text, " {mode}");
540 if mode == "share"
541 && let Some(id) = &target.list_rule_id
542 {
543 let _ = write!(text, " {}", name(id));
544 }
545 }
546 text
547}
548
549fn parameter_text(parameter: &Parameter) -> String {
550 match ¶meter.value {
551 ParameterValue::Id(id) => id.split('.').map(name).collect::<Vec<_>>().join("."),
552 ParameterValue::String(value) => quote_fhirpath_string(value),
553 ParameterValue::Boolean(value) => value.to_string(),
554 ParameterValue::Integer(value) => value.to_string(),
555 ParameterValue::Decimal(value) => value.to_string(),
556 ParameterValue::Date(value) | ParameterValue::DateTime(value) => format!("@{value}"),
557 ParameterValue::Time(value) => format!("@T{value}"),
558 }
559}
560
561fn name(text: &str) -> String {
563 let plain = text
564 .chars()
565 .next()
566 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
567 && text.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
568 if plain {
569 text.to_owned()
570 } else {
571 format!("`{}`", text.replace('\\', "\\\\").replace('`', "\\`"))
572 }
573}
574
575fn code_text(code: &str) -> String {
576 let plain = !code.is_empty() && code.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
577 let starts_like_name = code
578 .chars()
579 .next()
580 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
581 if plain && (starts_like_name || code.chars().all(|c| c.is_ascii_digit())) {
582 code.to_owned()
583 } else {
584 double_quoted(code)
585 }
586}
587
588fn double_quoted(text: &str) -> String {
589 format!("\"{}\"", text.replace('\\', "\\\\").replace('"', "\\\""))
590}