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 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 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 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
343fn 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
365const LEGACY_DEFAULT_GROUP: &str = "DefaultMappingGroupAnonymousAlias";
367
368fn 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
383fn 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
400fn 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
419fn 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
430fn 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 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(¶meter_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 ¶meter.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
562fn 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}