1use crate::writer::{Writer, comment_lines, escape_string, verbatim_lines};
24use alloc::string::{String, ToString};
25use alloc::vec::Vec;
26use zerodds_lang_cst::common::{Doc, Ident, Literal, Visibility};
27use zerodds_lang_cst::swift::{
28 Aggregate, AggregateDecl, Argument, ConstDecl, Decl, EnumCase, EnumDecl, Expr, File, FuncDecl,
29 Init, Method, Param, Property, ProtocolDecl, StaticLet, Stmt, Type, TypeAliasDecl,
30};
31
32#[must_use]
34pub fn file(file: &File) -> String {
35 let mut w = Writer::spaces4();
36 if !file.doc.is_empty() {
37 comment_lines(&mut w, "// ", file.doc.as_lines());
38 }
39 if !file.imports.is_empty() {
40 w.blank_line();
41 for import in &file.imports {
42 let mut line = String::from("import ");
43 line.push_str(import.as_str());
44 w.line(&line);
45 }
46 }
47 for decl in &file.decls {
48 w.blank_line();
49 render_decl(&mut w, decl);
50 }
51 w.finish()
52}
53
54#[must_use]
56pub fn decl(node: &Decl) -> String {
57 let mut w = Writer::spaces4();
58 render_decl(&mut w, node);
59 w.finish()
60}
61
62#[must_use]
64pub fn type_(node: &Type) -> String {
65 type_text(node)
66}
67
68#[must_use]
70pub fn ident(name: &Ident) -> String {
71 name.as_str().to_string()
72}
73
74fn render_decl(w: &mut Writer, node: &Decl) {
75 match node {
76 Decl::Aggregate(decl) => render_aggregate(w, decl),
77 Decl::Enum(decl) => render_enum(w, decl),
78 Decl::TypeAlias(decl) => render_type_alias(w, decl),
79 Decl::Constant(decl) => render_const(w, decl),
80 Decl::Protocol(decl) => render_protocol(w, decl),
81 Decl::Func(decl) => render_func(w, decl),
82 Decl::Comment(lines) => doc(w, lines),
83 Decl::Verbatim(text) => verbatim_lines(w, text),
84 }
85}
86
87fn render_func(w: &mut Writer, node: &FuncDecl) {
88 doc(w, &node.doc);
89 let mut head = visibility_text(node.visibility);
90 head.push_str("func ");
91 head.push_str(node.name.as_str());
92 head.push('(');
93 let params: Vec<String> = node.params.iter().map(param_text).collect();
94 head.push_str(¶ms.join(", "));
95 head.push(')');
96 if node.throws {
97 head.push_str(" throws");
98 }
99 if let Some(returns) = &node.returns {
100 head.push_str(" -> ");
101 head.push_str(&type_text(returns));
102 }
103 head.push_str(" {");
104 w.line(&head);
105 w.indent();
106 for stmt in &node.body {
107 render_stmt(w, stmt);
108 }
109 w.dedent();
110 w.line("}");
111}
112
113fn render_aggregate(w: &mut Writer, node: &AggregateDecl) {
114 doc(w, &node.doc);
115 let mut head = visibility_text(node.visibility);
116 head.push_str(match node.kind {
117 Aggregate::Struct => "struct ",
118 Aggregate::Class => "final class ",
119 });
120 head.push_str(node.name.as_str());
121 if !node.generics.is_empty() {
122 head.push('<');
123 let parts: Vec<&str> = node.generics.iter().map(Ident::as_str).collect();
124 head.push_str(&parts.join(", "));
125 head.push('>');
126 }
127 if !node.conformances.is_empty() {
128 head.push_str(": ");
129 let parts: Vec<&str> = node.conformances.iter().map(Ident::as_str).collect();
130 head.push_str(&parts.join(", "));
131 }
132 head.push_str(" {");
133 w.line(&head);
134 w.indent();
135 for text in &node.body_text.begin {
136 verbatim_lines(w, text);
137 }
138 for property in &node.properties {
139 render_property(w, property);
140 }
141 for static_ in &node.statics {
142 render_static(w, static_);
143 }
144 for init in &node.inits {
145 render_init(w, init);
146 }
147 for text in &node.body_text.end {
148 verbatim_lines(w, text);
149 }
150 w.dedent();
151 w.line("}");
152}
153
154fn render_property(w: &mut Writer, node: &Property) {
155 doc(w, &node.doc);
156 let mut line = visibility_text(node.visibility);
157 line.push_str(if node.mutable { "var " } else { "let " });
158 line.push_str(node.name.as_str());
159 line.push_str(": ");
160 line.push_str(&type_text(&node.type_));
161 w.line(&line);
162}
163
164fn render_static(w: &mut Writer, node: &StaticLet) {
165 doc(w, &node.doc);
166 let mut line = visibility_text(node.visibility);
167 line.push_str("static let ");
168 line.push_str(node.name.as_str());
169 if let Some(type_) = &node.type_ {
170 line.push_str(": ");
171 line.push_str(&type_text(type_));
172 }
173 line.push_str(" = ");
174 line.push_str(&expr_text(&node.value));
175 w.line(&line);
176}
177
178fn render_init(w: &mut Writer, node: &Init) {
183 doc(w, &node.doc);
184 let mut head = visibility_text(node.visibility);
185 head.push_str("init(");
186 let params: Vec<String> = node.params.iter().map(param_text).collect();
187 head.push_str(¶ms.join(", "));
188 head.push(')');
189 if node.body.is_empty() {
190 head.push_str(" {}");
191 w.line(&head);
192 return;
193 }
194 head.push_str(" {");
195 w.line(&head);
196 w.indent();
197 for stmt in &node.body {
198 render_stmt(w, stmt);
199 }
200 w.dedent();
201 w.line("}");
202}
203
204fn render_stmt(w: &mut Writer, node: &Stmt) {
211 match node {
212 Stmt::AssignProperty { property, value } => {
213 let mut line = String::from("self.");
214 line.push_str(property.as_str());
215 line.push_str(" = ");
216 line.push_str(&expr_text(value));
217 w.line(&line);
218 }
219 Stmt::Var { name, type_, value } => {
220 let mut line = String::from("var ");
221 line.push_str(name.as_str());
222 if let Some(type_) = type_ {
223 line.push_str(": ");
224 line.push_str(&type_text(type_));
225 }
226 if let Some(value) = value {
227 line.push_str(" = ");
228 line.push_str(&expr_text(value));
229 }
230 w.line(&line);
231 }
232 Stmt::Assign { place, value } => {
233 let mut line = expr_text(place);
234 line.push_str(" = ");
235 line.push_str(&expr_text(value));
236 w.line(&line);
237 }
238 Stmt::Expr(value) => w.line(&expr_text(value)),
239 Stmt::Return(value) => match value {
240 Some(value) => {
241 let mut line = String::from("return ");
242 line.push_str(&expr_text(value));
243 w.line(&line);
244 }
245 None => w.line("return"),
246 },
247 Stmt::If {
248 condition,
249 then,
250 otherwise,
251 } => render_if(w, condition, then, otherwise.as_deref()),
252 Stmt::ForRange {
253 counter,
254 bound,
255 body,
256 } => {
257 let mut head = String::from("for ");
258 head.push_str(counter.as_str());
259 head.push_str(" in 0..<");
260 head.push_str(&tight_operand(bound));
261 head.push_str(" {");
262 w.line(&head);
263 w.indent();
264 for stmt in body {
265 render_stmt(w, stmt);
266 }
267 w.dedent();
268 w.line("}");
269 }
270 Stmt::While { condition, body } => {
271 let mut head = String::from("while ");
272 head.push_str(&expr_text(condition));
273 head.push_str(" {");
274 w.line(&head);
275 w.indent();
276 for stmt in body {
277 render_stmt(w, stmt);
278 }
279 w.dedent();
280 w.line("}");
281 }
282 Stmt::Break => w.line("break"),
283 Stmt::Comment(lines) => {
284 if !lines.is_empty() {
285 comment_lines(w, "// ", lines.as_lines());
286 }
287 }
288 }
289}
290
291fn render_if(w: &mut Writer, condition: &Expr, then: &[Stmt], otherwise: Option<&[Stmt]>) {
295 let mut head = String::from("if ");
296 head.push_str(&expr_text(condition));
297 head.push_str(" {");
298 w.line(&head);
299 render_if_tail(w, then, otherwise);
300}
301
302fn render_if_tail(w: &mut Writer, then: &[Stmt], otherwise: Option<&[Stmt]>) {
316 let mut then = then;
317 let mut otherwise = otherwise;
318 loop {
319 w.indent();
320 for stmt in then {
321 render_stmt(w, stmt);
322 }
323 w.dedent();
324 let Some(branch) = otherwise else {
325 w.line("}");
326 return;
327 };
328 if let [
329 Stmt::If {
330 condition: next_condition,
331 then: next_then,
332 otherwise: next_otherwise,
333 },
334 ] = branch
335 {
336 let mut head = String::from("} else if ");
337 head.push_str(&expr_text(next_condition));
338 head.push_str(" {");
339 w.line(&head);
340 then = next_then;
341 otherwise = next_otherwise.as_deref();
342 continue;
343 }
344 w.line("} else {");
345 w.indent();
346 for stmt in branch {
347 render_stmt(w, stmt);
348 }
349 w.dedent();
350 w.line("}");
351 return;
352 }
353}
354
355fn tight_operand(bound: &Expr) -> String {
359 match bound {
360 Expr::Binary { .. } | Expr::Try(_) => {
361 let mut out = String::from("(");
362 out.push_str(&expr_text(bound));
363 out.push(')');
364 out
365 }
366 other => expr_text(other),
367 }
368}
369
370fn param_text(param: &Param) -> String {
371 let mut out = String::from(param.label.as_str());
372 if let Some(internal) = ¶m.internal_name {
373 out.push(' ');
374 out.push_str(internal.as_str());
375 }
376 out.push_str(": ");
377 out.push_str(&type_text(¶m.type_));
378 out
379}
380
381fn render_enum(w: &mut Writer, node: &EnumDecl) {
382 doc(w, &node.doc);
383 let mut head = visibility_text(node.visibility);
384 head.push_str("enum ");
385 head.push_str(node.name.as_str());
386 if let Some(raw) = &node.raw_type {
387 head.push_str(": ");
388 head.push_str(&type_text(raw));
389 }
390 head.push_str(" {");
391 w.line(&head);
392 w.indent();
393 for case in &node.cases {
394 render_case(w, case);
395 }
396 w.dedent();
397 w.line("}");
398}
399
400fn render_case(w: &mut Writer, node: &EnumCase) {
401 doc(w, &node.doc);
402 let mut line = String::from("case ");
403 line.push_str(node.name.as_str());
404 if let Some(value) = &node.raw_value {
405 line.push_str(" = ");
406 line.push_str(&expr_text(value));
407 }
408 w.line(&line);
409}
410
411fn render_type_alias(w: &mut Writer, node: &TypeAliasDecl) {
412 doc(w, &node.doc);
413 let mut line = visibility_text(node.visibility);
414 line.push_str("typealias ");
415 line.push_str(node.name.as_str());
416 line.push_str(" = ");
417 line.push_str(&type_text(&node.type_));
418 w.line(&line);
419}
420
421fn render_const(w: &mut Writer, node: &ConstDecl) {
422 doc(w, &node.doc);
423 let mut line = visibility_text(node.visibility);
424 line.push_str("let ");
425 line.push_str(node.name.as_str());
426 if let Some(type_) = &node.type_ {
427 line.push_str(": ");
428 line.push_str(&type_text(type_));
429 }
430 line.push_str(" = ");
431 line.push_str(&expr_text(&node.value));
432 w.line(&line);
433}
434
435fn render_protocol(w: &mut Writer, node: &ProtocolDecl) {
436 doc(w, &node.doc);
437 let mut head = visibility_text(node.visibility);
438 head.push_str("protocol ");
439 head.push_str(node.name.as_str());
440 head.push_str(" {");
441 w.line(&head);
442 w.indent();
443 for method in &node.methods {
444 render_method(w, method);
445 }
446 w.dedent();
447 w.line("}");
448}
449
450fn render_method(w: &mut Writer, node: &Method) {
451 doc(w, &node.doc);
452 let mut line = String::from("func ");
453 line.push_str(node.name.as_str());
454 line.push('(');
455 let params: Vec<String> = node.params.iter().map(param_text).collect();
456 line.push_str(¶ms.join(", "));
457 line.push(')');
458 if node.throws {
459 line.push_str(" throws");
460 }
461 if let Some(returns) = &node.returns {
462 line.push_str(" -> ");
463 line.push_str(&type_text(returns));
464 }
465 w.line(&line);
466}
467
468fn type_text(node: &Type) -> String {
472 match node {
473 Type::Primitive(primitive) => primitive.as_str().to_string(),
474 Type::Named(name) => name.as_str().to_string(),
475 Type::Generic { base, arguments } => {
476 let mut out = String::from(base.as_str());
477 out.push('<');
478 let parts: Vec<String> = arguments.iter().map(type_text).collect();
479 out.push_str(&parts.join(", "));
480 out.push('>');
481 out
482 }
483 Type::Array(element) => {
484 let mut out = String::from("[");
485 out.push_str(&type_text(element));
486 out.push(']');
487 out
488 }
489 Type::Optional(inner) => {
490 let mut out = type_text(inner);
491 out.push('?');
492 out
493 }
494 }
495}
496
497fn expr_text(node: &Expr) -> String {
504 match node {
505 Expr::Literal(literal) => literal_text(literal),
506 Expr::Name(name) => name.as_str().to_string(),
507 Expr::Binary { op, lhs, rhs } => {
508 let mut out = expr_text(lhs);
509 out.push(' ');
510 out.push_str(op);
511 out.push(' ');
512 out.push_str(&expr_text(rhs));
513 out
514 }
515 Expr::Call { callee, args } => {
516 let mut out = String::from(callee.as_str());
517 out.push('(');
518 let parts: Vec<String> = args.iter().map(argument_text).collect();
519 out.push_str(&parts.join(", "));
520 out.push(')');
521 out
522 }
523 Expr::Paren(inner) => {
524 let mut out = String::from("(");
525 out.push_str(&expr_text(inner));
526 out.push(')');
527 out
528 }
529 Expr::Member { value, name } => {
530 let mut out = expr_text(value);
531 out.push('.');
532 out.push_str(name.as_str());
533 out
534 }
535 Expr::Invoke {
536 receiver,
537 name,
538 args,
539 } => {
540 let mut out = match receiver {
541 Some(receiver) => {
542 let mut head = expr_text(receiver);
543 head.push('.');
544 head
545 }
546 None => String::new(),
547 };
548 out.push_str(name.as_str());
549 out.push('(');
550 let parts: Vec<String> = args.iter().map(argument_text).collect();
551 out.push_str(&parts.join(", "));
552 out.push(')');
553 out
554 }
555 Expr::Index { value, index } => {
556 let mut out = expr_text(value);
557 out.push('[');
558 out.push_str(&expr_text(index));
559 out.push(']');
560 out
561 }
562 Expr::Unary { op, value } => {
563 let mut out = String::from(*op);
564 out.push_str(&expr_text(value));
565 out
566 }
567 Expr::ForceUnwrap(value) => {
568 let mut out = expr_text(value);
569 out.push('!');
570 out
571 }
572 Expr::ArrayLit(elements) => {
573 let mut out = String::from("[");
574 let parts: Vec<String> = elements.iter().map(expr_text).collect();
575 out.push_str(&parts.join(", "));
576 out.push(']');
577 out
578 }
579 Expr::RepeatingArray {
580 element,
581 repeated,
582 count,
583 } => {
584 let mut out = String::from("[");
585 out.push_str(&type_text(element));
586 out.push_str("](repeating: ");
587 out.push_str(&expr_text(repeated));
588 out.push_str(", count: ");
589 out.push_str(&expr_text(count));
590 out.push(')');
591 out
592 }
593 Expr::TypeSelf(type_) => {
594 let mut out = type_text(type_);
595 out.push_str(".self");
596 out
597 }
598 Expr::Try(inner) => {
603 let mut out = String::from("try ");
604 out.push_str(&expr_text(inner));
605 out
606 }
607 }
608}
609
610fn argument_text(arg: &Argument) -> String {
611 let mut out = String::new();
612 if let Some(label) = &arg.label {
613 out.push_str(label.as_str());
614 out.push_str(": ");
615 }
616 out.push_str(&expr_text(&arg.value));
617 out
618}
619
620fn literal_text(literal: &Literal) -> String {
621 match literal {
622 Literal::Int(text) | Literal::Float(text) => text.clone(),
623 Literal::Bool(value) => {
624 if *value {
625 "true".to_string()
626 } else {
627 "false".to_string()
628 }
629 }
630 Literal::Char(ch) => alloc::format!("{}", u32::from(*ch)),
636 Literal::Str(text) => {
637 let mut out = String::from("\"");
638 out.push_str(&escape_string(text));
639 out.push('"');
640 out
641 }
642 Literal::Bytes(bytes) => {
643 let mut out = String::from("[");
644 let parts: Vec<String> = bytes.iter().map(|b| alloc::format!("0x{b:02X}")).collect();
645 out.push_str(&parts.join(", "));
646 out.push(']');
647 out
648 }
649 Literal::Null => "nil".to_string(),
650 }
651}
652
653fn visibility_text(visibility: Visibility) -> String {
658 match visibility {
659 Visibility::Public => "public ".to_string(),
660 Visibility::Internal => String::new(),
661 Visibility::Private => "private ".to_string(),
662 }
663}
664
665fn doc(w: &mut Writer, doc: &Doc) {
666 if doc.is_empty() {
667 return;
668 }
669 comment_lines(w, "/// ", doc.as_lines());
670}
671
672#[cfg(test)]
673mod tests {
674 #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
675 use super::*;
676 use zerodds_lang_cst::swift::Primitive;
677
678 fn int32() -> Type {
679 Type::Primitive(Primitive::Int32)
680 }
681
682 #[test]
683 fn a_struct_writes_its_properties_and_its_public_initialiser() {
684 let decl = Decl::Aggregate(AggregateDecl {
685 doc: Doc::none(),
686 visibility: Visibility::Public,
687 kind: Aggregate::Struct,
688 name: Ident::new("Pose"),
689 generics: Vec::new(),
690 conformances: Vec::new(),
691 properties: alloc::vec![Property {
692 doc: Doc::none(),
693 visibility: Visibility::Public,
694 mutable: true,
695 name: Ident::new("x"),
696 type_: int32(),
697 }],
698 statics: Vec::new(),
699 body_text: zerodds_lang_cst::common::BodyText::none(),
700 inits: alloc::vec![Init {
701 doc: Doc::none(),
702 visibility: Visibility::Public,
703 params: alloc::vec![Param {
704 label: Ident::new("x"),
705 internal_name: None,
706 type_: int32(),
707 }],
708 body: alloc::vec![Stmt::AssignProperty {
709 property: Ident::new("x"),
710 value: Expr::Name(Ident::new("x")),
711 }],
712 }],
713 });
714 assert_eq!(
715 super::decl(&decl),
716 "public struct Pose {\n public var x: Int32\n public init(x: Int32) {\n \
717 self.x = x\n }\n}\n"
718 );
719 }
720
721 #[test]
722 fn an_empty_initialiser_stays_on_one_line() {
723 let init = Init {
724 doc: Doc::none(),
725 visibility: Visibility::Public,
726 params: Vec::new(),
727 body: Vec::new(),
728 };
729 let mut w = Writer::spaces4();
730 render_init(&mut w, &init);
731 assert_eq!(w.finish(), "public init() {}\n");
732 }
733
734 #[test]
735 fn a_parameter_with_a_second_name_writes_both() {
736 let param = Param {
737 label: Ident::new("`self`"),
738 internal_name: Some(Ident::new("zdSelf")),
739 type_: int32(),
740 };
741 assert_eq!(param_text(¶m), "`self` zdSelf: Int32");
742 }
743
744 #[test]
745 fn a_class_is_written_final() {
746 let decl = Decl::Aggregate(AggregateDecl {
747 doc: Doc::none(),
748 visibility: Visibility::Public,
749 kind: Aggregate::Class,
750 name: Ident::new("ZdExternal"),
751 generics: alloc::vec![Ident::new("Value")],
752 conformances: Vec::new(),
753 properties: Vec::new(),
754 statics: Vec::new(),
755 body_text: zerodds_lang_cst::common::BodyText::none(),
756 inits: Vec::new(),
757 });
758 assert_eq!(
759 super::decl(&decl),
760 "public final class ZdExternal<Value> {\n}\n"
761 );
762 }
763
764 #[test]
765 fn an_option_set_writes_its_conformance_and_its_flags() {
766 let decl = Decl::Aggregate(AggregateDecl {
767 doc: Doc::none(),
768 visibility: Visibility::Public,
769 kind: Aggregate::Struct,
770 name: Ident::new("Flags"),
771 generics: Vec::new(),
772 conformances: alloc::vec![Ident::new("OptionSet")],
773 properties: Vec::new(),
774 statics: alloc::vec![StaticLet {
775 doc: Doc::none(),
776 visibility: Visibility::Public,
777 name: Ident::new("READY"),
778 type_: None,
779 value: Expr::Call {
780 callee: Ident::new("Flags"),
781 args: alloc::vec![Argument {
782 label: Some(Ident::new("rawValue")),
783 value: Expr::Binary {
784 op: "<<",
785 lhs: alloc::boxed::Box::new(Expr::int(1)),
786 rhs: alloc::boxed::Box::new(Expr::int(4)),
787 },
788 }],
789 },
790 }],
791 body_text: zerodds_lang_cst::common::BodyText::none(),
792 inits: Vec::new(),
793 });
794 assert_eq!(
795 super::decl(&decl),
796 "public struct Flags: OptionSet {\n public static let READY = \
797 Flags(rawValue: 1 << 4)\n}\n"
798 );
799 }
800
801 #[test]
802 fn an_enum_writes_every_raw_value_out() {
803 let decl = Decl::Enum(EnumDecl {
804 doc: Doc::none(),
805 visibility: Visibility::Public,
806 name: Ident::new("Mode"),
807 raw_type: Some(int32()),
808 cases: alloc::vec![
809 EnumCase {
810 doc: Doc::none(),
811 name: Ident::new("IDLE"),
812 raw_value: Some(Expr::int(0)),
813 },
814 EnumCase {
815 doc: Doc::none(),
816 name: Ident::new("ACTIVE"),
817 raw_value: Some(Expr::int(4)),
818 },
819 ],
820 });
821 assert_eq!(
822 super::decl(&decl),
823 "public enum Mode: Int32 {\n case IDLE = 0\n case ACTIVE = 4\n}\n"
824 );
825 }
826
827 #[test]
828 fn an_enum_with_no_cases_writes_no_raw_type() {
829 let decl = Decl::Enum(EnumDecl {
830 doc: Doc::none(),
831 visibility: Visibility::Public,
832 name: Ident::new("Empty"),
833 raw_type: None,
834 cases: Vec::new(),
835 });
836 assert_eq!(super::decl(&decl), "public enum Empty {\n}\n");
837 }
838
839 #[test]
840 fn a_protocol_writes_its_requirements() {
841 let decl = Decl::Protocol(ProtocolDecl {
842 doc: Doc::none(),
843 visibility: Visibility::Public,
844 name: Ident::new("Control"),
845 methods: alloc::vec![
846 Method {
847 doc: Doc::none(),
848 name: Ident::new("go"),
849 params: alloc::vec![Param {
850 label: Ident::new("target"),
851 internal_name: None,
852 type_: Type::named("Pose"),
853 }],
854 throws: false,
855 returns: None,
856 },
857 Method {
858 doc: Doc::none(),
859 name: Ident::new("status"),
860 params: Vec::new(),
861 throws: false,
862 returns: Some(int32()),
863 },
864 ],
865 });
866 assert_eq!(
867 super::decl(&decl),
868 "public protocol Control {\n func go(target: Pose)\n func status() -> Int32\n}\n"
869 );
870 }
871
872 #[test]
873 fn a_typealias_and_a_constant_each_write_one_line() {
874 assert_eq!(
875 super::decl(&Decl::TypeAlias(TypeAliasDecl {
876 doc: Doc::none(),
877 visibility: Visibility::Public,
878 name: Ident::new("Distance"),
879 type_: int32(),
880 })),
881 "public typealias Distance = Int32\n"
882 );
883 assert_eq!(
884 super::decl(&Decl::Constant(ConstDecl {
885 doc: Doc::none(),
886 visibility: Visibility::Public,
887 name: Ident::new("LIMIT"),
888 type_: Some(Type::Primitive(Primitive::UInt8)),
889 value: Expr::int(8),
890 })),
891 "public let LIMIT: UInt8 = 8\n"
892 );
893 }
894
895 #[test]
896 fn a_collection_type_writes_the_sugar_and_not_the_generic() {
897 assert_eq!(type_(&Type::array(int32())), "[Int32]");
901 assert_eq!(type_(&Type::array(Type::array(int32()))), "[[Int32]]");
902 assert_eq!(type_(&Type::optional(int32())), "Int32?");
903 assert_eq!(type_(&Type::optional(Type::array(int32()))), "[Int32]?");
904 }
905
906 #[test]
907 fn a_generic_type_writes_its_arguments() {
908 assert_eq!(
909 type_(&Type::generic(
910 "ZdExternal",
911 alloc::vec![Type::named("Node")]
912 )),
913 "ZdExternal<Node>"
914 );
915 }
916
917 #[test]
918 fn an_internal_declaration_writes_no_keyword_because_that_is_the_default() {
919 assert_eq!(visibility_text(Visibility::Internal), "");
920 assert_eq!(visibility_text(Visibility::Public), "public ");
921 assert_eq!(visibility_text(Visibility::Private), "private ");
922 }
923
924 #[test]
925 fn a_string_literal_is_escaped_and_a_char_is_its_code_point() {
926 assert_eq!(
927 literal_text(&Literal::Str("a\"b".to_string())),
928 "\"a\\\"b\""
929 );
930 assert_eq!(literal_text(&Literal::Char('A')), "65");
931 assert_eq!(literal_text(&Literal::Null), "nil");
932 assert_eq!(literal_text(&Literal::Bool(true)), "true");
933 }
934
935 #[test]
936 fn a_file_writes_its_header_as_a_plain_comment() {
937 let out = file(&File {
938 doc: Doc::lines(&["Generated by ZeroDDS. Do not edit."]),
939 imports: alloc::vec![Ident::new("Foundation")],
940 decls: Vec::new(),
941 });
942 assert_eq!(
943 out,
944 "// Generated by ZeroDDS. Do not edit.\n\nimport Foundation\n"
945 );
946 }
947}