1use crate::formatter::config::FormatConfig;
4use crate::parser::ast::{
5 parse_source, Ast, AstNode, FileMetadata, ImportDecl, ImportItem, ImportSpecifier,
6};
7use crate::policy::Expression;
8use std::fmt;
9
10#[derive(Debug)]
12pub enum FormatError {
13 ParseError(String),
15 InternalError(String),
17}
18
19impl fmt::Display for FormatError {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 match self {
22 FormatError::ParseError(msg) => write!(f, "Parse error: {}", msg),
23 FormatError::InternalError(msg) => write!(f, "Internal error: {}", msg),
24 }
25 }
26}
27
28impl std::error::Error for FormatError {}
29
30pub fn format(source: &str, config: FormatConfig) -> Result<String, FormatError> {
53 if config.preserve_comments {
55 format_preserving_comments(source, config)
56 } else {
57 format_without_comments(source, config)
58 }
59}
60
61fn format_without_comments(source: &str, config: FormatConfig) -> Result<String, FormatError> {
63 let ast = parse_source(source).map_err(|e| FormatError::ParseError(e.to_string()))?;
64
65 let mut formatter = Formatter::new(config, None);
66 formatter.format_ast(&ast);
67
68 Ok(formatter.output)
69}
70
71pub fn format_preserving_comments(
78 source: &str,
79 config: FormatConfig,
80) -> Result<String, FormatError> {
81 use crate::formatter::comments::CommentedSource;
82
83 let commented = CommentedSource::new(source);
84 let ast = parse_source(source).map_err(|e| FormatError::ParseError(e.to_string()))?;
85
86 let mut formatter = Formatter::new(config, Some(commented));
87 formatter.format_ast(&ast);
88
89 Ok(formatter.output)
90}
91
92struct Formatter {
94 config: FormatConfig,
95 output: String,
96 indent_level: usize,
97 commented_source: Option<crate::formatter::comments::CommentedSource>,
99}
100
101impl Formatter {
102 fn new(
103 config: FormatConfig,
104 commented_source: Option<crate::formatter::comments::CommentedSource>,
105 ) -> Self {
106 Self {
107 config,
108 output: String::new(),
109 indent_level: 0,
110 commented_source,
111 }
112 }
113
114 fn format_ast(&mut self, ast: &Ast) {
116 let header_comments: Vec<_> = self
119 .commented_source
120 .as_ref()
121 .map(|cs| cs.file_header_comments.clone())
122 .unwrap_or_default();
123
124 for comment in &header_comments {
125 self.write("// ");
126 self.write(&comment.text);
127 self.newline();
128 }
129 if !header_comments.is_empty() {
130 self.newline();
131 }
132
133 self.format_file_metadata(&ast.metadata);
135
136 for (i, decl) in ast.declarations.iter().enumerate() {
138 if i > 0 || !ast.metadata.imports.is_empty() || ast.metadata.namespace.is_some() {
139 self.newline();
140 }
141 self.format_declaration(&decl.node);
142 }
143
144 if self.config.trailing_newline && !self.output.ends_with('\n') {
146 self.output.push('\n');
147 }
148 }
149
150 fn format_file_metadata(&mut self, meta: &FileMetadata) {
152 if let Some(ref ns) = meta.namespace {
154 self.write("@namespace ");
155 self.write_string_literal(ns);
156 self.newline();
157 }
158 if let Some(ref version) = meta.version {
159 self.write("@version ");
160 self.write_string_literal(version);
161 self.newline();
162 }
163 if let Some(ref owner) = meta.owner {
164 self.write("@owner ");
165 self.write_string_literal(owner);
166 self.newline();
167 }
168 if let Some(ref profile) = meta.profile {
169 self.write("@profile ");
170 self.write_string_literal(profile);
171 self.newline();
172 }
173
174 if (meta.namespace.is_some()
176 || meta.version.is_some()
177 || meta.owner.is_some()
178 || meta.profile.is_some())
179 && !meta.imports.is_empty()
180 {
181 self.newline();
182 }
183
184 let mut imports = meta.imports.clone();
186 if self.config.sort_imports {
187 imports.sort_by(|a, b| a.from_module.cmp(&b.from_module));
188 }
189
190 for import in &imports {
191 self.format_import(import);
192 self.newline();
193 }
194 }
195
196 fn format_import(&mut self, import: &ImportDecl) {
198 self.write("import ");
199 match &import.specifier {
200 ImportSpecifier::Named(items) => {
201 self.write("{ ");
202 for (i, item) in items.iter().enumerate() {
203 if i > 0 {
204 self.write(", ");
205 }
206 self.format_import_item(item);
207 }
208 self.write(" }");
209 }
210 ImportSpecifier::Wildcard(alias) => {
211 self.write("* as ");
212 self.write(alias);
213 }
214 }
215 self.write(" from ");
216 self.write_string_literal(&import.from_module);
217 }
218
219 fn format_import_item(&mut self, item: &ImportItem) {
221 self.write(&item.name);
222 if let Some(ref alias) = item.alias {
223 self.write(" as ");
224 self.write(alias);
225 }
226 }
227
228 fn format_declaration(&mut self, node: &AstNode) {
230 match node {
231 AstNode::Export(inner) => {
232 self.write("export ");
233 self.format_declaration(&inner.node);
234 }
235 AstNode::Entity {
236 name,
237 version,
238 annotations,
239 domain,
240 } => {
241 self.write("Entity ");
242 self.write_string_literal(name);
243 if let Some(v) = version {
244 self.write(" v");
245 self.write(v);
246 }
247 if let Some(replaces) = annotations.get("replaces") {
249 if let Some(s) = replaces.as_str() {
250 self.newline();
251 self.indent();
252 self.write_indent();
253 self.write("@replaces ");
254 if s.contains(" v") {
256 let parts: Vec<&str> = s.splitn(2, " v").collect();
257 self.write_string_literal(parts[0]);
258 if parts.len() > 1 {
259 self.write(" v");
260 self.write(parts[1]);
261 }
262 } else {
263 self.write_string_literal(s);
264 }
265 self.dedent();
266 }
267 }
268 if let Some(changes) = annotations.get("changes") {
269 if let Some(arr) = changes.as_array() {
270 self.newline();
271 self.indent();
272 self.write_indent();
273 self.write("@changes [");
274 for (i, change) in arr.iter().enumerate() {
275 if i > 0 {
276 self.write(", ");
277 }
278 if let Some(s) = change.as_str() {
279 self.write_string_literal(s);
280 }
281 }
282 self.write("]");
283 self.dedent();
284 }
285 }
286 if let Some(d) = domain {
287 self.write(" in ");
288 self.write(d);
289 }
290 self.newline();
291 }
292 AstNode::Resource {
293 name,
294 annotations,
295 unit_name,
296 domain,
297 } => {
298 self.write("Resource ");
299 self.write_string_literal(name);
300 if let Some(replaces) = annotations.get("replaces") {
302 if let Some(s) = replaces.as_str() {
303 self.newline();
304 self.indent();
305 self.write_indent();
306 self.write("@replaces ");
307 if s.contains(" v") {
309 let parts: Vec<&str> = s.splitn(2, " v").collect();
310 self.write_string_literal(parts[0]);
311 if parts.len() > 1 {
312 self.write(" v");
313 self.write(parts[1]);
314 }
315 } else {
316 self.write_string_literal(s);
317 }
318 self.dedent();
319 }
320 }
321 if let Some(changes) = annotations.get("changes") {
322 if let Some(arr) = changes.as_array() {
323 self.newline();
324 self.indent();
325 self.write_indent();
326 self.write("@changes [");
327 for (i, change) in arr.iter().enumerate() {
328 if i > 0 {
329 self.write(", ");
330 }
331 if let Some(s) = change.as_str() {
332 self.write_string_literal(s);
333 }
334 }
335 self.write("]");
336 self.dedent();
337 }
338 }
339 if let Some(u) = unit_name {
340 self.write(" ");
341 self.write(u);
342 }
343 if let Some(d) = domain {
344 self.write(" in ");
345 self.write(d);
346 }
347 self.newline();
348 }
349 AstNode::Flow {
350 resource_name,
351 annotations,
352 from_entity,
353 to_entity,
354 quantity,
355 } => {
356 self.write("Flow ");
357 self.write_string_literal(resource_name);
358 if let Some(replaces) = annotations.get("replaces") {
360 if let Some(s) = replaces.as_str() {
361 self.newline();
362 self.indent();
363 self.write_indent();
364 self.write("@replaces ");
365 if s.contains(" v") {
366 let parts: Vec<&str> = s.splitn(2, " v").collect();
367 self.write_string_literal(parts[0]);
368 if parts.len() > 1 {
369 self.write(" v");
370 self.write(parts[1]);
371 }
372 } else {
373 self.write_string_literal(s);
374 }
375 self.dedent();
376 }
377 }
378 if let Some(changes) = annotations.get("changes") {
379 if let Some(arr) = changes.as_array() {
380 self.newline();
381 self.indent();
382 self.write_indent();
383 self.write("@changes [");
384 for (i, change) in arr.iter().enumerate() {
385 if i > 0 {
386 self.write(", ");
387 }
388 if let Some(s) = change.as_str() {
389 self.write_string_literal(s);
390 }
391 }
392 self.write("]");
393 self.dedent();
394 }
395 }
396 self.write(" from ");
397 self.write_string_literal(from_entity);
398 self.write(" to ");
399 self.write_string_literal(to_entity);
400 if let Some(q) = quantity {
401 self.write(" quantity ");
402 self.write(&q.to_string());
403 }
404 self.newline();
405 }
406 AstNode::Pattern { name, regex } => {
407 self.write("Pattern ");
408 self.write_string_literal(name);
409 self.write(" matches ");
410 self.write_string_literal(regex);
411 self.newline();
412 }
413 AstNode::Role { name, domain } => {
414 self.write("Role ");
415 self.write_string_literal(name);
416 if let Some(d) = domain {
417 self.write(" in ");
418 self.write(d);
419 }
420 self.newline();
421 }
422 AstNode::Relation {
423 name,
424 subject_role,
425 predicate,
426 object_role,
427 via_flow,
428 } => {
429 self.write("Relation ");
430 self.write_string_literal(name);
431 self.newline();
432 self.indent();
433 self.write_indent();
434 self.write("subject: ");
435 self.write_string_literal(subject_role);
436 self.newline();
437 self.write_indent();
438 self.write("predicate: ");
439 self.write_string_literal(predicate);
440 self.newline();
441 self.write_indent();
442 self.write("object: ");
443 self.write_string_literal(object_role);
444 if let Some(flow) = via_flow {
445 self.newline();
446 self.write_indent();
447 self.write("via: flow ");
448 self.write_string_literal(flow);
449 }
450 self.dedent();
451 self.newline();
452 }
453 AstNode::Dimension { name } => {
454 self.write("Dimension ");
455 self.write_string_literal(name);
456 self.newline();
457 }
458 AstNode::UnitDeclaration {
459 symbol,
460 dimension,
461 factor,
462 base_unit,
463 } => {
464 self.write("Unit ");
465 self.write_string_literal(symbol);
466 self.write(" of ");
467 self.write_string_literal(dimension);
468 self.write(" factor ");
469 self.write(&factor.to_string());
470 self.write(" base ");
471 self.write_string_literal(base_unit);
472 self.newline();
473 }
474 AstNode::Policy {
475 name,
476 version,
477 metadata,
478 expression,
479 } => {
480 self.write("Policy ");
481 self.write(name);
482 if let (Some(kind), Some(modality), Some(priority)) =
483 (&metadata.kind, &metadata.modality, metadata.priority)
484 {
485 self.write(" per ");
486 self.write(&format!("{}", kind));
487 self.write(" ");
488 self.write(&format!("{}", modality));
489 self.write(" priority ");
490 self.write(&priority.to_string());
491 }
492 if let Some(ref rationale) = metadata.rationale {
494 self.newline();
495 self.indent();
496 self.write_indent();
497 self.write("@rationale ");
498 self.write_string_literal(rationale);
499 self.dedent();
500 }
501 if !metadata.tags.is_empty() {
502 self.newline();
503 self.indent();
504 self.write_indent();
505 self.write("@tags [");
506 for (i, tag) in metadata.tags.iter().enumerate() {
507 if i > 0 {
508 self.write(", ");
509 }
510 self.write_string_literal(tag);
511 }
512 self.write("]");
513 self.dedent();
514 }
515 if let Some(v) = version {
516 self.newline();
517 self.indent();
518 self.write_indent();
519 self.write("v");
520 self.write(v);
521 self.dedent();
522 }
523 self.newline();
524 self.indent();
525 self.write_indent();
526 self.write("as: ");
527 self.format_expression(expression);
528 self.dedent();
529 self.newline();
530 }
531 AstNode::Instance {
532 name,
533 entity_type,
534 fields,
535 } => {
536 self.write("Instance ");
537 self.write(name);
538 self.write(" of ");
539 self.write_string_literal(entity_type);
540 if !fields.is_empty() {
541 self.write(" {");
542 self.newline();
543 self.indent();
544 let mut sorted_fields: Vec<_> = fields.iter().collect();
545 sorted_fields.sort_by_key(|(k, _)| *k);
546 for (i, (field_name, expr)) in sorted_fields.iter().enumerate() {
547 self.write_indent();
548 self.write(field_name);
549 self.write(": ");
550 self.format_expression(expr);
551 if i < sorted_fields.len() - 1 {
552 self.write(",");
553 }
554 self.newline();
555 }
556 self.dedent();
557 self.write_indent();
558 self.write("}");
559 }
560 self.newline();
561 }
562 AstNode::ConceptChange {
563 name,
564 from_version,
565 to_version,
566 migration_policy,
567 breaking_change,
568 } => {
569 self.write("ConceptChange ");
570 self.write_string_literal(name);
571 self.newline();
572 self.indent();
573 self.write_indent();
574 self.write("@from_version v");
575 self.write(from_version);
576 self.newline();
577 self.write_indent();
578 self.write("@to_version v");
579 self.write(to_version);
580 self.newline();
581 self.write_indent();
582 self.write("@migration_policy ");
583 self.write(migration_policy);
584 self.newline();
585 self.write_indent();
586 self.write("@breaking_change ");
587 self.write(if *breaking_change { "true" } else { "false" });
588 self.dedent();
589 self.newline();
590 }
591 AstNode::Metric {
592 name,
593 expression,
594 metadata,
595 } => {
596 self.write("Metric ");
597 self.write_string_literal(name);
598 self.write(" as:");
599 self.newline();
600 self.indent();
601 self.write_indent();
602 self.format_expression(expression);
603 if let Some(ref interval) = metadata.refresh_interval {
605 self.newline();
606 self.write_indent();
607 self.write("@refresh_interval ");
608 self.write(&interval.num_seconds().to_string());
609 self.write(" \"seconds\"");
610 }
611 if let Some(ref unit) = metadata.unit {
612 self.newline();
613 self.write_indent();
614 self.write("@unit ");
615 self.write_string_literal(unit);
616 }
617 if let Some(threshold) = &metadata.threshold {
618 self.newline();
619 self.write_indent();
620 self.write("@threshold ");
621 self.write(&threshold.to_string());
622 }
623 if let Some(ref severity) = metadata.severity {
624 self.newline();
625 self.write_indent();
626 self.write("@severity ");
627 self.write_string_literal(&format!("{:?}", severity).to_lowercase());
628 }
629 if let Some(target) = &metadata.target {
630 self.newline();
631 self.write_indent();
632 self.write("@target ");
633 self.write(&target.to_string());
634 }
635 if let Some(ref window) = metadata.window {
636 self.newline();
637 self.write_indent();
638 self.write("@window ");
639 self.write(&window.num_seconds().to_string());
640 self.write(" \"seconds\"");
641 }
642 self.dedent();
643 self.newline();
644 }
645 AstNode::MappingDecl {
646 name,
647 target,
648 rules,
649 } => {
650 self.write("Mapping ");
651 self.write_string_literal(name);
652 self.write(" for ");
653 self.write(&format!("{}", target).to_lowercase());
654 self.write(" {");
655 self.newline();
656 self.indent();
657 for rule in rules {
658 self.write_indent();
659 self.write(&rule.primitive_type);
660 self.write(" ");
661 self.write_string_literal(&rule.primitive_name);
662 self.write(" -> ");
663 self.write(&rule.target_type);
664 self.write(" { ");
665 let mut first = true;
666 for (k, v) in &rule.fields {
667 if !first {
668 self.write(", ");
669 }
670 self.write_string_literal(k);
671 self.write(": ");
672 self.write(&v.to_string());
673 first = false;
674 }
675 self.write(" }");
676 self.newline();
677 }
678 self.dedent();
679 self.write_indent();
680 self.write("}");
681 self.newline();
682 }
683 AstNode::ProjectionDecl {
684 name,
685 target,
686 overrides,
687 } => {
688 self.write("Projection ");
689 self.write_string_literal(name);
690 self.write(" for ");
691 self.write(&format!("{}", target).to_lowercase());
692 self.write(" {");
693 self.newline();
694 self.indent();
695 for over in overrides {
696 self.write_indent();
697 self.write(&over.primitive_type);
698 self.write(" ");
699 self.write_string_literal(&over.primitive_name);
700 self.write(" { ");
701 let mut first = true;
702 for (k, v) in &over.fields {
703 if !first {
704 self.write(", ");
705 }
706 self.write_string_literal(k);
707 self.write(": ");
708 self.write(&v.to_string());
709 first = false;
710 }
711 self.write(" }");
712 self.newline();
713 }
714 self.dedent();
715 self.write_indent();
716 self.write("}");
717 self.newline();
718 }
719 }
720 }
721
722 fn format_expression(&mut self, expr: &Expression) {
726 self.write(&format!("{}", expr));
727 }
728
729 fn write(&mut self, s: &str) {
732 self.output.push_str(s);
733 }
734
735 fn write_string_literal(&mut self, s: &str) {
736 self.output.push('"');
737 for c in s.chars() {
739 match c {
740 '"' => self.output.push_str("\\\""),
741 '\\' => self.output.push_str("\\\\"),
742 '\n' => self.output.push_str("\\n"),
743 '\r' => self.output.push_str("\\r"),
744 '\t' => self.output.push_str("\\t"),
745 _ => self.output.push(c),
746 }
747 }
748 self.output.push('"');
749 }
750
751 fn newline(&mut self) {
752 self.output.push('\n');
753 }
754
755 fn write_indent(&mut self) {
756 let indent = self.config.indent_string();
757 for _ in 0..self.indent_level {
758 self.output.push_str(&indent);
759 }
760 }
761
762 fn indent(&mut self) {
763 self.indent_level += 1;
764 }
765
766 fn dedent(&mut self) {
767 if self.indent_level > 0 {
768 self.indent_level -= 1;
769 }
770 }
771}
772
773#[cfg(test)]
774mod tests {
775 use super::*;
776
777 #[test]
778 fn test_format_entity_basic() {
779 let input = r#"Entity "Foo""#;
780 let result = format(input, FormatConfig::default()).unwrap();
781 assert!(result.contains("Entity \"Foo\""));
782 assert!(result.ends_with('\n'));
783 }
784
785 #[test]
786 fn test_format_entity_with_domain() {
787 let input = r#"Entity "Bar" in test"#;
788 let result = format(input, FormatConfig::default()).unwrap();
789 assert!(result.contains("Entity \"Bar\" in test"));
790 }
791
792 #[test]
793 fn test_format_resource() {
794 let input = r#"Resource "Money" USD in finance"#;
795 let result = format(input, FormatConfig::default()).unwrap();
796 assert!(result.contains("Resource \"Money\" USD in finance"));
797 }
798
799 #[test]
800 fn test_format_flow() {
801 let input = r#"Flow "Money" from "A" to "B" quantity 100"#;
802 let result = format(input, FormatConfig::default()).unwrap();
803 assert!(result.contains("Flow \"Money\" from \"A\" to \"B\" quantity 100"));
804 }
805
806 #[test]
807 fn test_format_idempotent() {
808 let input = r#"Entity "Test" in foo"#;
809 let once = format(input, FormatConfig::default()).unwrap();
810 let twice = format(&once, FormatConfig::default()).unwrap();
811 assert_eq!(once, twice, "Formatting should be idempotent");
812 }
813
814 #[test]
815 fn test_format_multiple_declarations() {
816 let input = r#"
817Entity "A"
818Entity "B"
819Resource "R" units
820"#;
821 let result = format(input, FormatConfig::default()).unwrap();
822 assert!(result.contains("Entity \"A\""));
823 assert!(result.contains("Entity \"B\""));
824 assert!(result.contains("Resource \"R\""));
825 }
826
827 #[test]
828 fn test_format_with_tabs() {
829 let input = r#"
830Relation "Test"
831 subject: "A"
832 predicate: "rel"
833 object: "B"
834"#;
835 let config = FormatConfig::default().with_tabs();
836 let result = format(input, config).unwrap();
837 assert!(result.contains('\t'), "Should use tabs for indentation");
838 }
839
840 #[test]
841 fn test_format_imports_sorted() {
842 let input = r#"
843import { B } from "z.sea"
844import { A } from "a.sea"
845Entity "Test"
846"#;
847 let config = FormatConfig::default();
848 let result = format(input, config).unwrap();
849 let a_pos = result.find("a.sea").unwrap();
851 let z_pos = result.find("z.sea").unwrap();
852 assert!(a_pos < z_pos, "Imports should be sorted alphabetically");
853 }
854
855 #[test]
856 fn test_format_policy_with_expression() {
857 let input = r#"Policy test as: x = 5"#;
858 let result = format(input, FormatConfig::default()).unwrap();
859 assert!(result.contains("Policy test"));
861 assert!(result.contains("as:"));
862 assert!(!result.contains("Binary {"), "Should not use Debug format");
864 }
865
866 #[test]
867 fn test_format_instance_with_fields() {
868 let input = r#"
869Instance test_user of "User" {
870 name: "Alice",
871 age: 30
872}
873"#;
874 let result = format(input, FormatConfig::default()).unwrap();
875 assert!(result.contains("Instance test_user of \"User\""));
876 assert!(result.contains("name:"));
877 assert!(result.contains("age:"));
878 }
879
880 #[test]
881 fn test_format_preserves_header_comments() {
882 let input = r#"// This is a header comment
883// Second line
884Entity "Foo"
885"#;
886 let result = format(input, FormatConfig::default()).unwrap();
887 assert!(result.contains("// This is a header comment"));
888 assert!(result.contains("// Second line"));
889 assert!(result.contains("Entity \"Foo\""));
890 }
891
892 #[test]
893 fn test_format_without_comments() {
894 let input = r#"// Comment
895Entity "Foo"
896"#;
897 let config = FormatConfig {
898 preserve_comments: false,
899 ..Default::default()
900 };
901 let result = format(input, config).unwrap();
902 assert!(!result.contains("// Comment"));
904 assert!(result.contains("Entity \"Foo\""));
905 }
906}