Skip to main content

typewriter_engine/
emit.rs

1//! Render and write generated output files for target languages.
2
3use anyhow::Result;
4use std::collections::BTreeMap;
5use std::fs;
6use std::path::{Path, PathBuf};
7use typewriter_core::config::TypewriterConfig;
8use typewriter_core::ir::{Language, TypeDef};
9use typewriter_core::mapper::TypeMapper;
10
11use crate::TypeSpec;
12
13#[derive(Debug, Clone)]
14pub struct GeneratedFile {
15    pub type_name: String,
16    pub language: Language,
17    pub output_path: PathBuf,
18    pub content: String,
19    pub source_path: PathBuf,
20}
21
22/// Render all expected generated files for a set of type specs.
23pub fn render_specs(
24    specs: &[TypeSpec],
25    project_root: &Path,
26    config: &TypewriterConfig,
27    lang_filter: &[Language],
28    skip_unavailable: bool,
29) -> Result<Vec<GeneratedFile>> {
30    let mut files = Vec::new();
31
32    for spec in specs {
33        for target in effective_targets(&spec.targets, lang_filter) {
34            let mut rendered = render_single(
35                &spec.type_def,
36                spec.source_path.clone(),
37                spec.zod_schema,
38                target,
39                project_root,
40                config,
41                skip_unavailable,
42            )?;
43            files.append(&mut rendered);
44        }
45    }
46
47    Ok(files)
48}
49
50/// Render with output path deduplication (last writer wins on path collisions).
51pub fn render_specs_deduped(
52    specs: &[TypeSpec],
53    project_root: &Path,
54    config: &TypewriterConfig,
55    lang_filter: &[Language],
56    skip_unavailable: bool,
57) -> Result<Vec<GeneratedFile>> {
58    let files = render_specs(specs, project_root, config, lang_filter, skip_unavailable)?;
59    let mut by_path = BTreeMap::new();
60    for file in files {
61        by_path.insert(file.output_path.clone(), file);
62    }
63    Ok(by_path.into_values().collect())
64}
65
66pub fn write_generated_files(files: &[GeneratedFile]) -> Result<()> {
67    for file in files {
68        if let Some(parent) = file.output_path.parent() {
69            fs::create_dir_all(parent)?;
70        }
71        fs::write(&file.output_path, &file.content)?;
72    }
73    Ok(())
74}
75
76pub fn language_label(language: Language) -> &'static str {
77    match language {
78        Language::TypeScript => "typescript",
79        Language::Python => "python",
80        Language::Go => "go",
81        Language::Swift => "swift",
82        Language::Kotlin => "kotlin",
83    }
84}
85
86pub fn file_extension(language: Language) -> &'static str {
87    match language {
88        Language::TypeScript => "ts",
89        Language::Python => "py",
90        Language::Go => "go",
91        Language::Swift => "swift",
92        Language::Kotlin => "kt",
93    }
94}
95
96pub fn output_dir_for_language(config: &TypewriterConfig, language: Language) -> &str {
97    match language {
98        Language::TypeScript => config.ts_output_dir(),
99        Language::Python => config.py_output_dir(),
100        Language::Go => config.go_output_dir(),
101        Language::Swift => config.swift_output_dir(),
102        Language::Kotlin => config.kotlin_output_dir(),
103    }
104}
105
106fn effective_targets(spec_targets: &[Language], lang_filter: &[Language]) -> Vec<Language> {
107    if lang_filter.is_empty() {
108        return spec_targets.to_vec();
109    }
110
111    spec_targets
112        .iter()
113        .copied()
114        .filter(|lang| lang_filter.contains(lang))
115        .collect()
116}
117
118fn render_single(
119    type_def: &TypeDef,
120    source_path: PathBuf,
121    zod_override: Option<bool>,
122    language: Language,
123    project_root: &Path,
124    config: &TypewriterConfig,
125    _skip_unavailable: bool,
126) -> Result<Vec<GeneratedFile>> {
127    match language {
128        Language::TypeScript => {
129            #[cfg(feature = "typescript")]
130            {
131                let mut mapper = typewriter_typescript::TypeScriptMapper::new()
132                    .with_readonly(config.ts_readonly());
133                if let Some(style) = config.ts_file_style() {
134                    mapper = mapper.with_file_style(style);
135                }
136
137                let output_dir = project_root.join(config.ts_output_dir());
138
139                let type_file = render_with_mapper(
140                    &mapper,
141                    type_def,
142                    source_path.clone(),
143                    language,
144                    output_dir.clone(),
145                );
146
147                let mut files = vec![type_file];
148
149                if zod_override.unwrap_or(config.ts_zod_enabled()) {
150                    files.push(GeneratedFile {
151                        type_name: type_def.name().to_string(),
152                        language,
153                        output_path: output_dir.join(mapper.zod_output_filename(type_def.name())),
154                        content: mapper.emit_zod_type_def(type_def),
155                        source_path,
156                    });
157                }
158
159                return Ok(files);
160            }
161            #[cfg(not(feature = "typescript"))]
162            {
163                if _skip_unavailable {
164                    return Ok(vec![]);
165                }
166                anyhow::bail!("language 'typescript' is not enabled in this build")
167            }
168        }
169        Language::Python => {
170            #[cfg(feature = "python")]
171            {
172                let mut mapper = typewriter_python::PythonMapper::new();
173                if let Some(style) = config.py_file_style() {
174                    mapper = mapper.with_file_style(style);
175                }
176                let output_dir = project_root.join(config.py_output_dir());
177                return Ok(vec![render_with_mapper(
178                    &mapper,
179                    type_def,
180                    source_path,
181                    language,
182                    output_dir,
183                )]);
184            }
185            #[cfg(not(feature = "python"))]
186            {
187                if _skip_unavailable {
188                    return Ok(vec![]);
189                }
190                anyhow::bail!("language 'python' is not enabled in this build")
191            }
192        }
193        Language::Go => {
194            #[cfg(feature = "go")]
195            {
196                let mut mapper =
197                    typewriter_go::GoMapper::new().with_package_name(config.go_package_name());
198                if let Some(style) = config.go_file_style() {
199                    mapper = mapper.with_file_style(style);
200                }
201                let output_dir = project_root.join(config.go_output_dir());
202                return Ok(vec![render_with_mapper(
203                    &mapper,
204                    type_def,
205                    source_path,
206                    language,
207                    output_dir,
208                )]);
209            }
210            #[cfg(not(feature = "go"))]
211            {
212                if _skip_unavailable {
213                    return Ok(vec![]);
214                }
215                anyhow::bail!("language 'go' is not enabled in this build")
216            }
217        }
218        Language::Swift => {
219            #[cfg(feature = "swift")]
220            {
221                let mut mapper = typewriter_swift::SwiftMapper::new();
222                if let Some(style) = config.swift_file_style() {
223                    mapper = mapper.with_file_style(style);
224                }
225                let output_dir = project_root.join(config.swift_output_dir());
226                return Ok(vec![render_with_mapper(
227                    &mapper,
228                    type_def,
229                    source_path,
230                    language,
231                    output_dir,
232                )]);
233            }
234            #[cfg(not(feature = "swift"))]
235            {
236                if _skip_unavailable {
237                    return Ok(vec![]);
238                }
239                anyhow::bail!("language 'swift' is not enabled in this build")
240            }
241        }
242        Language::Kotlin => {
243            #[cfg(feature = "kotlin")]
244            {
245                let mut mapper = typewriter_kotlin::KotlinMapper::new()
246                    .with_package_name(config.kotlin_package_name());
247                if let Some(style) = config.kotlin_file_style() {
248                    mapper = mapper.with_file_style(style);
249                }
250                let output_dir = project_root.join(config.kotlin_output_dir());
251                return Ok(vec![render_with_mapper(
252                    &mapper,
253                    type_def,
254                    source_path,
255                    language,
256                    output_dir,
257                )]);
258            }
259            #[cfg(not(feature = "kotlin"))]
260            {
261                if _skip_unavailable {
262                    return Ok(vec![]);
263                }
264                anyhow::bail!("language 'kotlin' is not enabled in this build")
265            }
266        }
267    }
268}
269
270fn render_with_mapper<M: TypeMapper>(
271    mapper: &M,
272    type_def: &TypeDef,
273    source_path: PathBuf,
274    language: Language,
275    output_dir: PathBuf,
276) -> GeneratedFile {
277    let filename = mapper.output_filename(type_def.name());
278    let content = mapper.emit_type_def(type_def);
279
280    GeneratedFile {
281        type_name: type_def.name().to_string(),
282        language,
283        output_path: output_dir.join(filename),
284        content,
285        source_path,
286    }
287}