1use crate::parser::ast::{
2 Ast, AstNode, FileMetadata, ImportDecl, ImportItem, ImportSpecifier, MappingRule,
3 ProjectionOverride,
4};
5use serde_json::Value as JsonValue;
6use std::collections::HashMap;
7use std::fmt::Write;
8
9#[derive(Copy, Clone)]
10enum ObjectStyle {
11 ColonSeparated,
12 ArrowSeparated,
13}
14
15pub struct PrettyPrinter {
16 indent_width: usize,
17 #[allow(dead_code)]
18 max_line_length: usize,
19 #[allow(dead_code)]
20 trailing_commas: bool,
21}
22
23impl Default for PrettyPrinter {
24 fn default() -> Self {
25 Self {
26 indent_width: 4,
27 max_line_length: 80,
28 trailing_commas: false,
29 }
30 }
31}
32
33impl PrettyPrinter {
34 pub fn new() -> Self {
35 Self::default()
36 }
37
38 pub fn with_trailing_commas(mut self, trailing: bool) -> Self {
40 self.trailing_commas = trailing;
41 self
42 }
43
44 pub fn print(&self, ast: &Ast) -> String {
45 let mut sections = Vec::new();
46 let mut header = String::new();
47 self.write_metadata(&mut header, &ast.metadata);
48 if !header.trim().is_empty() {
49 sections.push(header.trim_end().to_string());
50 }
51
52 for decl in &ast.declarations {
53 sections.push(self.format_node(&decl.node, 0));
54 }
55
56 let mut output = sections.join("\n\n");
57 if !output.ends_with('\n') {
58 output.push('\n');
59 }
60 output
61 }
62
63 fn indent(&self, level: usize) -> String {
64 " ".repeat(self.indent_width * level)
65 }
66
67 fn quote(&self, value: &str) -> String {
68 serde_json::to_string(value).unwrap_or_else(|_| format!("\"{}\"", value))
69 }
70
71 fn write_metadata(&self, output: &mut String, metadata: &FileMetadata) {
72 let mut wrote_header = false;
73
74 if let Some(ns) = &metadata.namespace {
75 let _ = writeln!(output, "@namespace {}", self.quote(ns));
76 wrote_header = true;
77 }
78 if let Some(version) = &metadata.version {
79 let _ = writeln!(output, "@version {}", self.quote(version));
80 wrote_header = true;
81 }
82 if let Some(owner) = &metadata.owner {
83 let _ = writeln!(output, "@owner {}", self.quote(owner));
84 wrote_header = true;
85 }
86
87 for import in &metadata.imports {
88 let _ = writeln!(output, "{}", self.format_import(import));
89 wrote_header = true;
90 }
91
92 if wrote_header {
93 let _ = writeln!(output);
94 }
95 }
96
97 fn format_import(&self, import: &ImportDecl) -> String {
98 let specifier = match &import.specifier {
99 ImportSpecifier::Named(items) => {
100 let rendered: Vec<String> = items
101 .iter()
102 .map(|item| self.render_import_item(item))
103 .collect();
104 format!("{{ {} }}", rendered.join(", "))
105 }
106 ImportSpecifier::Wildcard(alias) => format!("* as {}", alias),
107 };
108 format!(
109 "Import {} from {}",
110 specifier,
111 self.quote(&import.from_module)
112 )
113 }
114
115 fn render_import_item(&self, item: &ImportItem) -> String {
116 match &item.alias {
117 Some(alias) => format!("{} as {}", item.name, alias),
118 None => item.name.clone(),
119 }
120 }
121
122 fn format_node(&self, node: &AstNode, indent_level: usize) -> String {
123 match node {
124 AstNode::Export(inner) => self.format_export(&inner.node, indent_level),
125 AstNode::Entity {
126 name,
127 version,
128 annotations,
129 domain,
130 } => self.format_entity(name, version, annotations, domain),
131 AstNode::Resource {
132 name,
133 annotations,
134 unit_name,
135 domain,
136 } => self.format_resource(name, annotations, unit_name.as_deref(), domain.as_deref()),
137 AstNode::Flow {
138 resource_name,
139 annotations,
140 from_entity,
141 to_entity,
142 quantity,
143 } => self.format_flow(
144 resource_name,
145 annotations,
146 from_entity,
147 to_entity,
148 *quantity,
149 ),
150 AstNode::Pattern { name, regex } => self.format_pattern(name, regex),
151 AstNode::Role { name, domain } => self.format_role(name, domain),
152 AstNode::Relation {
153 name,
154 subject_role,
155 predicate,
156 object_role,
157 via_flow,
158 } => self.format_relation(name, subject_role, predicate, object_role, via_flow),
159 AstNode::Dimension { name } => format!("Dimension {}", self.quote(name)),
160 AstNode::UnitDeclaration {
161 symbol,
162 dimension,
163 factor,
164 base_unit,
165 } => self.format_unit(symbol, dimension, factor, base_unit),
166 AstNode::Policy {
167 name,
168 version,
169 metadata,
170 expression,
171 } => self.format_policy(name, version, metadata, expression),
172 AstNode::Instance {
173 name,
174 entity_type,
175 fields,
176 } => self.format_instance(name, entity_type, fields),
177 AstNode::ConceptChange {
178 name,
179 from_version,
180 to_version,
181 migration_policy,
182 breaking_change,
183 } => self.format_concept_change(
184 name,
185 from_version,
186 to_version,
187 migration_policy,
188 *breaking_change,
189 ),
190 AstNode::Metric {
191 name,
192 expression,
193 metadata,
194 } => self.format_metric(name, expression, metadata),
195 AstNode::MappingDecl {
196 name,
197 target,
198 rules,
199 } => self.format_mapping(name, target, rules),
200 AstNode::ProjectionDecl {
201 name,
202 target,
203 overrides,
204 } => self.format_projection(name, target, overrides),
205 }
206 }
207
208 fn format_export(&self, node: &AstNode, indent_level: usize) -> String {
209 let inner = self.format_node(node, indent_level);
210 let mut lines = inner.lines();
211 if let Some(first) = lines.next() {
212 let mut rendered = vec![format!("Export {}", first)];
213 rendered.extend(lines.map(|line| line.to_string()));
214 rendered.join("\n")
215 } else {
216 String::new()
217 }
218 }
219
220 fn format_entity(
221 &self,
222 name: &str,
223 version: &Option<String>,
224 annotations: &HashMap<String, JsonValue>,
225 domain: &Option<String>,
226 ) -> String {
227 let mut lines = Vec::new();
228 let mut head = format!("Entity {}", self.quote(name));
229 if let Some(v) = version {
230 head.push_str(&format!(" v{}", v));
231 }
232 lines.push(head);
233
234 if let Some(replaces) = annotations.get("replaces").and_then(JsonValue::as_str) {
235 lines.push(format!(
236 "{}@replaces {}",
237 self.indent(1),
238 self.format_replaces_annotation(replaces)
239 ));
240 }
241 if let Some(changes) = annotations.get("changes").and_then(JsonValue::as_array) {
242 let rendered = changes
243 .iter()
244 .filter_map(JsonValue::as_str)
245 .map(|c| self.quote(c))
246 .collect::<Vec<_>>()
247 .join(", ");
248 lines.push(format!("{}@changes [{}]", self.indent(1), rendered));
249 }
250 if let Some(ns) = domain {
251 lines.push(format!("{}in {}", self.indent(1), ns));
252 }
253
254 lines.join("\n")
255 }
256
257 fn format_replaces_annotation(&self, value: &str) -> String {
258 if let Some((name, version)) = value.rsplit_once(" v") {
259 format!("{} v{}", self.quote(name), version)
260 } else {
261 self.quote(value)
262 }
263 }
264
265 fn format_resource(
266 &self,
267 name: &str,
268 annotations: &HashMap<String, JsonValue>,
269 unit: Option<&str>,
270 domain: Option<&str>,
271 ) -> String {
272 let mut lines = Vec::new();
273 let head = format!("Resource {}", self.quote(name));
274 lines.push(head);
275
276 if let Some(replaces) = annotations.get("replaces").and_then(JsonValue::as_str) {
277 lines.push(format!(
278 "{}@replaces {}",
279 self.indent(1),
280 self.format_replaces_annotation(replaces)
281 ));
282 }
283 if let Some(changes) = annotations.get("changes").and_then(JsonValue::as_array) {
284 let rendered = changes
285 .iter()
286 .filter_map(JsonValue::as_str)
287 .map(|c| self.quote(c))
288 .collect::<Vec<_>>()
289 .join(", ");
290 lines.push(format!("{}@changes [{}]", self.indent(1), rendered));
291 }
292
293 if unit.is_some() || domain.is_some() {
295 if lines.len() == 1 {
296 if let Some(u) = unit {
298 lines[0].push_str(&format!(" {}", u));
299 }
300 if let Some(ns) = domain {
301 lines[0].push_str(&format!(" in {}", ns));
302 }
303 } else {
304 let mut suffix = String::new();
306 if let Some(u) = unit {
307 suffix.push_str(u);
308 }
309 if let Some(ns) = domain {
310 if !suffix.is_empty() {
311 suffix.push(' ');
312 }
313 suffix.push_str(&format!("in {}", ns));
314 }
315 if !suffix.is_empty() {
316 lines.push(format!("{}{}", self.indent(1), suffix));
317 }
318 }
319 }
320
321 lines.join("\n")
322 }
323
324 fn format_flow(
325 &self,
326 resource: &str,
327 annotations: &HashMap<String, JsonValue>,
328 from: &str,
329 to: &str,
330 quantity: Option<i32>,
331 ) -> String {
332 let mut lines = Vec::new();
333 let head = format!("Flow {}", self.quote(resource));
334 lines.push(head);
335
336 if let Some(replaces) = annotations.get("replaces").and_then(JsonValue::as_str) {
337 lines.push(format!(
338 "{}@replaces {}",
339 self.indent(1),
340 self.format_replaces_annotation(replaces)
341 ));
342 }
343 if let Some(changes) = annotations.get("changes").and_then(JsonValue::as_array) {
344 let rendered = changes
345 .iter()
346 .filter_map(JsonValue::as_str)
347 .map(|c| self.quote(c))
348 .collect::<Vec<_>>()
349 .join(", ");
350 lines.push(format!("{}@changes [{}]", self.indent(1), rendered));
351 }
352
353 let mut suffix = format!("from {} to {}", self.quote(from), self.quote(to));
355 if let Some(qty) = quantity {
356 suffix.push_str(&format!(" quantity {}", qty));
357 }
358
359 if lines.len() == 1 {
360 lines[0].push_str(&format!(" {}", suffix));
362 } else {
363 lines.push(format!("{}{}", self.indent(1), suffix));
365 }
366
367 lines.join("\n")
368 }
369
370 fn format_pattern(&self, name: &str, regex: &str) -> String {
371 format!("Pattern {} matches {}", self.quote(name), self.quote(regex))
372 }
373
374 fn format_role(&self, name: &str, domain: &Option<String>) -> String {
375 match domain {
376 Some(ns) => format!("Role {} in {}", self.quote(name), ns),
377 None => format!("Role {}", self.quote(name)),
378 }
379 }
380
381 fn format_relation(
382 &self,
383 name: &str,
384 subject: &str,
385 predicate: &str,
386 object: &str,
387 via_flow: &Option<String>,
388 ) -> String {
389 let mut lines = Vec::new();
390 lines.push(format!("Relation {}", self.quote(name)));
391 lines.push(format!(
392 "{}subject: {}",
393 self.indent(1),
394 self.quote(subject)
395 ));
396 lines.push(format!(
397 "{}predicate: {}",
398 self.indent(1),
399 self.quote(predicate)
400 ));
401 lines.push(format!("{}object: {}", self.indent(1), self.quote(object)));
402 if let Some(flow) = via_flow {
403 lines.push(format!("{}via: flow {}", self.indent(1), self.quote(flow)));
404 }
405 lines.join("\n")
406 }
407
408 fn format_unit(
409 &self,
410 symbol: &str,
411 dimension: &str,
412 factor: &rust_decimal::Decimal,
413 base_unit: &str,
414 ) -> String {
415 format!(
416 "Unit {} of {} factor {} base {}",
417 self.quote(symbol),
418 self.quote(dimension),
419 factor,
420 self.quote(base_unit)
421 )
422 }
423
424 fn format_policy(
425 &self,
426 name: &str,
427 version: &Option<String>,
428 metadata: &crate::parser::ast::PolicyMetadata,
429 expression: &crate::policy::Expression,
430 ) -> String {
431 let mut header = format!("Policy {}", name);
432 if let Some(kind) = &metadata.kind {
433 header.push_str(&format!(" per {}", kind));
434 }
435 if let Some(modality) = &metadata.modality {
436 header.push_str(&format!(" {}", modality));
437 }
438 if let Some(priority) = metadata.priority {
439 header.push_str(&format!(" priority {}", priority));
440 }
441 if let Some(rationale) = &metadata.rationale {
442 header.push_str(&format!(" @rationale {}", self.quote(rationale)));
443 }
444 if !metadata.tags.is_empty() {
445 let tags = metadata
446 .tags
447 .iter()
448 .map(|t| self.quote(t))
449 .collect::<Vec<_>>()
450 .join(", ");
451 header.push_str(&format!(" @tags [{}]", tags));
452 }
453 if let Some(v) = version {
454 header.push_str(&format!(" v{}", v));
455 }
456 header.push_str(" as:");
457
458 let mut lines = Vec::new();
459 lines.push(header);
460 lines.push(format!("{}{}", self.indent(1), expression));
461 lines.join("\n")
462 }
463
464 fn format_instance(
465 &self,
466 name: &str,
467 entity_type: &str,
468 fields: &HashMap<String, crate::policy::Expression>,
469 ) -> String {
470 if fields.is_empty() {
471 return format!("Instance {} of {}", name, self.quote(entity_type));
472 }
473
474 let mut lines = Vec::new();
475 lines.push(format!(
476 "Instance {} of {} {{",
477 name,
478 self.quote(entity_type)
479 ));
480
481 let mut entries: Vec<_> = fields.iter().collect();
482 entries.sort_by(|a, b| a.0.cmp(b.0));
483 for (idx, (field, value)) in entries.iter().enumerate() {
484 let is_last = idx == entries.len() - 1;
485 let suffix = if self.trailing_commas {
486 ","
487 } else if is_last {
488 ""
489 } else {
490 ","
491 };
492 lines.push(format!("{}{}: {}{}", self.indent(1), field, value, suffix));
493 }
494
495 lines.push("}".to_string());
496 lines.join("\n")
497 }
498
499 fn format_concept_change(
500 &self,
501 name: &str,
502 from_version: &str,
503 to_version: &str,
504 migration_policy: &str,
505 breaking_change: bool,
506 ) -> String {
507 let mut lines = Vec::new();
508 lines.push(format!("ConceptChange {}", self.quote(name)));
509 lines.push(format!("{}@from_version v{}", self.indent(1), from_version));
510 lines.push(format!("{}@to_version v{}", self.indent(1), to_version));
511 lines.push(format!(
512 "{}@migration_policy {}",
513 self.indent(1),
514 migration_policy
515 ));
516 lines.push(format!(
517 "{}@breaking_change {}",
518 self.indent(1),
519 breaking_change
520 ));
521 lines.join("\n")
522 }
523
524 fn format_metric(
525 &self,
526 name: &str,
527 expression: &crate::policy::Expression,
528 metadata: &crate::parser::ast::MetricMetadata,
529 ) -> String {
530 let mut lines = Vec::new();
531 lines.push(format!("Metric {} as:", self.quote(name)));
532 lines.push(format!("{}{}", self.indent(1), expression));
533
534 if let Some(refresh) = metadata.refresh_interval {
535 lines.push(format!(
536 "{}@refresh_interval {} \"seconds\"",
537 self.indent(1),
538 refresh.num_seconds()
539 ));
540 }
541 if let Some(unit) = &metadata.unit {
542 lines.push(format!("{}@unit {}", self.indent(1), self.quote(unit)));
543 }
544 if let Some(threshold) = metadata.threshold {
545 lines.push(format!("{}@threshold {}", self.indent(1), threshold));
546 }
547 if let Some(severity) = &metadata.severity {
548 lines.push(format!(
549 "{}@severity {}",
550 self.indent(1),
551 self.quote(&format!("{:?}", severity))
552 ));
553 }
554 if let Some(target) = metadata.target {
555 lines.push(format!("{}@target {}", self.indent(1), target));
556 }
557 if let Some(window) = metadata.window {
558 lines.push(format!(
559 "{}@window {} \"seconds\"",
560 self.indent(1),
561 window.num_seconds()
562 ));
563 }
564
565 lines.join("\n")
566 }
567
568 fn format_mapping(
569 &self,
570 name: &str,
571 target: &crate::parser::ast::TargetFormat,
572 rules: &[MappingRule],
573 ) -> String {
574 let mut lines = Vec::new();
575 lines.push(format!("Mapping {} for {} {{", self.quote(name), target));
576
577 for rule in rules {
578 let mut field_lines = Vec::new();
579 field_lines.push(format!(
580 "{}{} {} -> {} {{",
581 self.indent(1),
582 rule.primitive_type,
583 self.quote(&rule.primitive_name),
584 rule.target_type
585 ));
586
587 let mut fields: Vec<_> = rule.fields.iter().collect();
588 fields.sort_by(|a, b| a.0.cmp(b.0));
589 for (idx, (field, value)) in fields.iter().enumerate() {
590 let is_last = idx == fields.len() - 1;
591 let suffix = if self.trailing_commas {
592 ","
593 } else if is_last {
594 ""
595 } else {
596 ","
597 };
598 field_lines.push(format!(
599 "{}{}: {}{}",
600 self.indent(2),
601 field,
602 self.format_mapping_value(value, ObjectStyle::ColonSeparated),
603 suffix
604 ));
605 }
606 field_lines.push(format!("{}}}", self.indent(1)));
607 lines.push(field_lines.join("\n"));
608 }
609
610 lines.push("}".to_string());
611 lines.join("\n")
612 }
613
614 fn format_projection(
615 &self,
616 name: &str,
617 target: &crate::parser::ast::TargetFormat,
618 overrides: &[ProjectionOverride],
619 ) -> String {
620 let mut lines = Vec::new();
621 lines.push(format!("Projection {} for {} {{", self.quote(name), target));
622
623 for override_entry in overrides {
624 let mut override_lines = Vec::new();
625 override_lines.push(format!(
626 "{}{} {} {{",
627 self.indent(1),
628 override_entry.primitive_type,
629 self.quote(&override_entry.primitive_name)
630 ));
631
632 let mut fields: Vec<_> = override_entry.fields.iter().collect();
633 fields.sort_by(|a, b| a.0.cmp(b.0));
634 for (idx, (field, value)) in fields.iter().enumerate() {
635 let is_last = idx == fields.len() - 1;
636 let suffix = if self.trailing_commas {
637 ","
638 } else if is_last {
639 ""
640 } else {
641 ","
642 };
643 override_lines.push(format!(
644 "{}{}: {}{}",
645 self.indent(2),
646 field,
647 self.format_mapping_value(value, ObjectStyle::ArrowSeparated),
648 suffix
649 ));
650 }
651 override_lines.push(format!("{}}}", self.indent(1)));
652 lines.push(override_lines.join("\n"));
653 }
654
655 lines.push("}".to_string());
656 lines.join("\n")
657 }
658
659 fn format_mapping_value(&self, value: &JsonValue, object_style: ObjectStyle) -> String {
660 match value {
661 JsonValue::String(s) => self.quote(s),
662 JsonValue::Bool(b) => b.to_string(),
663 JsonValue::Number(n) => n.to_string(),
664 JsonValue::Object(map) => {
665 let mut parts: Vec<_> = map.iter().collect();
666 parts.sort_by(|a, b| a.0.cmp(b.0));
667 let rendered = parts
668 .into_iter()
669 .map(|(k, v)| {
670 let rendered_value = match v {
671 JsonValue::String(s) => self.quote(s),
672 JsonValue::Bool(b) => b.to_string(),
673 JsonValue::Number(n) => n.to_string(),
674 JsonValue::Object(_) | JsonValue::Array(_) => {
675 self.format_mapping_value(v, object_style)
677 }
678 _ => self.quote(&v.to_string()),
679 };
680 let separator = match object_style {
681 ObjectStyle::ColonSeparated => ":",
682 ObjectStyle::ArrowSeparated => "->",
683 };
684 format!("{} {} {}", self.quote(k), separator, rendered_value)
685 })
686 .collect::<Vec<_>>()
687 .join(", ");
688 format!("{{ {} }}", rendered)
689 }
690 JsonValue::Array(arr) => {
691 let items = arr
692 .iter()
693 .map(|v| self.format_mapping_value(v, object_style))
694 .collect::<Vec<_>>()
695 .join(", ");
696 format!("[{}]", items)
697 }
698 _ => self.quote(&value.to_string()),
699 }
700 }
701}