1use 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
22pub 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
50pub 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}