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