baobao_codegen_typescript/
code_file.rs

1//! CodeFile abstraction for structured TypeScript file generation.
2//!
3//! Provides a high-level API for generating TypeScript files with
4//! organized imports, body content, and exports sections.
5
6use baobao_codegen::builder::{CodeBuilder, CodeFragment, Indent, Renderable};
7
8use crate::ast::{Export, Import};
9
10/// A structured representation of a TypeScript file.
11///
12/// Organizes code into three sections: imports, body, and exports.
13/// Each section is rendered in order with appropriate blank lines.
14///
15/// # Example
16///
17/// ```ignore
18/// let file = CodeFile::new()
19///     .import(Import::new("boune").named("defineCommand"))
20///     .add(command_schema)
21///     .export(Export::new().named("fooCommand"))
22///     .render();
23/// ```
24#[derive(Default)]
25pub struct CodeFile {
26    imports: Vec<Import>,
27    body: Vec<Vec<CodeFragment>>,
28    exports: Vec<Export>,
29}
30
31impl CodeFile {
32    /// Create a new empty CodeFile.
33    pub fn new() -> Self {
34        Self::default()
35    }
36
37    /// Add an import statement.
38    pub fn import(mut self, import: Import) -> Self {
39        self.imports.push(import);
40        self
41    }
42
43    /// Add imports from an iterator.
44    pub fn imports(mut self, imports: impl IntoIterator<Item = Import>) -> Self {
45        self.imports.extend(imports);
46        self
47    }
48
49    /// Add a body element (any Renderable).
50    #[allow(clippy::should_implement_trait)]
51    pub fn add<R: Renderable>(mut self, node: R) -> Self {
52        self.body.push(node.to_fragments());
53        self
54    }
55
56    /// Add multiple body elements.
57    pub fn add_all<R: Renderable>(mut self, nodes: impl IntoIterator<Item = R>) -> Self {
58        for node in nodes {
59            self.body.push(node.to_fragments());
60        }
61        self
62    }
63
64    /// Add an export statement.
65    pub fn export(mut self, export: Export) -> Self {
66        self.exports.push(export);
67        self
68    }
69
70    /// Add exports from an iterator.
71    pub fn exports(mut self, exports: impl IntoIterator<Item = Export>) -> Self {
72        self.exports.extend(exports);
73        self
74    }
75
76    /// Render the file with TypeScript indentation (2 spaces).
77    pub fn render(&self) -> String {
78        self.render_with_indent(Indent::TYPESCRIPT)
79    }
80
81    /// Render the file with custom indentation.
82    pub fn render_with_indent(&self, indent: Indent) -> String {
83        let mut builder = CodeBuilder::new(indent);
84
85        // 1. Render imports
86        for import in &self.imports {
87            builder.emit(import);
88        }
89
90        // 2. Blank line between imports and body
91        if !self.imports.is_empty() && (!self.body.is_empty() || !self.exports.is_empty()) {
92            builder.push_blank();
93        }
94
95        // 3. Render body with blank lines between elements
96        for (i, fragments) in self.body.iter().enumerate() {
97            if i > 0 {
98                builder.push_blank();
99            }
100            for fragment in fragments {
101                builder.apply_fragment(fragment.clone());
102            }
103        }
104
105        // 4. Blank line before exports
106        if !self.body.is_empty() && !self.exports.is_empty() {
107            builder.push_blank();
108        }
109
110        // 5. Render exports
111        for export in &self.exports {
112            builder.emit(export);
113        }
114
115        builder.build()
116    }
117
118    /// Check if the file is empty.
119    pub fn is_empty(&self) -> bool {
120        self.imports.is_empty() && self.body.is_empty() && self.exports.is_empty()
121    }
122}
123
124/// A raw code fragment that implements Renderable.
125///
126/// Useful for adding raw code strings to CodeFile body.
127#[derive(Debug, Clone)]
128pub struct RawCode(String);
129
130impl RawCode {
131    /// Create a new raw code fragment.
132    pub fn new(code: impl Into<String>) -> Self {
133        Self(code.into())
134    }
135
136    /// Create a raw code fragment from multiple lines.
137    pub fn lines(lines: impl IntoIterator<Item = impl Into<String>>) -> Self {
138        Self(
139            lines
140                .into_iter()
141                .map(Into::into)
142                .collect::<Vec<_>>()
143                .join("\n"),
144        )
145    }
146}
147
148impl Renderable for RawCode {
149    fn to_fragments(&self) -> Vec<CodeFragment> {
150        self.0
151            .lines()
152            .map(|line| CodeFragment::Line(line.to_string()))
153            .collect()
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_empty_file() {
163        let file = CodeFile::new();
164        assert!(file.is_empty());
165        assert_eq!(file.render(), "");
166    }
167
168    #[test]
169    fn test_imports_only() {
170        let file = CodeFile::new().import(Import::new("boune").named("defineCommand"));
171        let code = file.render();
172        assert!(code.contains("import { defineCommand } from \"boune\";"));
173    }
174
175    #[test]
176    fn test_raw_code_body() {
177        let file = CodeFile::new().add(RawCode::new("const x = 1;"));
178        let code = file.render();
179        assert_eq!(code, "const x = 1;\n");
180    }
181
182    #[test]
183    fn test_full_file() {
184        let file = CodeFile::new()
185            .import(Import::new("boune").named("defineCommand"))
186            .add(RawCode::new("const cmd = defineCommand({});"))
187            .export(Export::new().named("cmd"));
188
189        let code = file.render();
190        assert!(code.contains("import { defineCommand }"));
191        assert!(code.contains("const cmd = defineCommand"));
192        assert!(code.contains("export { cmd }"));
193    }
194
195    #[test]
196    fn test_blank_lines_between_body() {
197        let file = CodeFile::new()
198            .add(RawCode::new("const a = 1;"))
199            .add(RawCode::new("const b = 2;"));
200
201        let code = file.render();
202        assert!(code.contains("const a = 1;\n\nconst b = 2;"));
203    }
204}