sea_core/parser/
printer.rs

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    /// Returns a configured PrettyPrinter with trailing commas enabled/disabled.
39    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        // Add unit and domain to first line if present
294        if unit.is_some() || domain.is_some() {
295            if lines.len() == 1 {
296                // No annotations, put on single line
297                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                // Has annotations, add unit/domain on separate line
305                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        // Add from/to/quantity
354        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            // No annotations, put on single line
361            lines[0].push_str(&format!(" {}", suffix));
362        } else {
363            // Has annotations, add on separate line
364            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                                // Recursively format nested objects/arrays so they are rendered correctly
676                                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}