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 Language::GraphQL => "graphql",
84 Language::JsonSchema => "json_schema",
85 }
86}
87
88pub fn file_extension(language: Language) -> &'static str {
89 match language {
90 Language::TypeScript => "ts",
91 Language::Python => "py",
92 Language::Go => "go",
93 Language::Swift => "swift",
94 Language::Kotlin => "kt",
95 Language::GraphQL => "graphql",
96 Language::JsonSchema => "schema.json",
97 }
98}
99
100pub fn output_dir_for_language(config: &TypewriterConfig, language: Language) -> &str {
101 match language {
102 Language::TypeScript => config.ts_output_dir(),
103 Language::Python => config.py_output_dir(),
104 Language::Go => config.go_output_dir(),
105 Language::Swift => config.swift_output_dir(),
106 Language::Kotlin => config.kotlin_output_dir(),
107 Language::GraphQL => config.graphql_output_dir(),
108 Language::JsonSchema => config.json_schema_output_dir(),
109 }
110}
111
112fn effective_targets(spec_targets: &[Language], lang_filter: &[Language]) -> Vec<Language> {
113 if lang_filter.is_empty() {
114 return spec_targets.to_vec();
115 }
116
117 spec_targets
118 .iter()
119 .copied()
120 .filter(|lang| lang_filter.contains(lang))
121 .collect()
122}
123
124fn render_single(
125 type_def: &TypeDef,
126 source_path: PathBuf,
127 zod_override: Option<bool>,
128 language: Language,
129 project_root: &Path,
130 config: &TypewriterConfig,
131 _skip_unavailable: bool,
132) -> Result<Vec<GeneratedFile>> {
133 match language {
134 Language::TypeScript => {
135 #[cfg(feature = "typescript")]
136 {
137 let mut mapper = typewriter_typescript::TypeScriptMapper::new()
138 .with_readonly(config.ts_readonly());
139 if let Some(style) = config.ts_file_style() {
140 mapper = mapper.with_file_style(style);
141 }
142
143 let output_dir = project_root.join(config.ts_output_dir());
144
145 let type_file = render_with_mapper(
146 &mapper,
147 type_def,
148 source_path.clone(),
149 language,
150 output_dir.clone(),
151 );
152
153 let mut files = vec![type_file];
154
155 if zod_override.unwrap_or(config.ts_zod_enabled()) {
156 files.push(GeneratedFile {
157 type_name: type_def.name().to_string(),
158 language,
159 output_path: output_dir.join(mapper.zod_output_filename(type_def.name())),
160 content: mapper.emit_zod_type_def(type_def),
161 source_path,
162 });
163 }
164
165 return Ok(files);
166 }
167 #[cfg(not(feature = "typescript"))]
168 {
169 if _skip_unavailable {
170 return Ok(vec![]);
171 }
172 anyhow::bail!("language 'typescript' is not enabled in this build")
173 }
174 }
175 Language::Python => {
176 #[cfg(feature = "python")]
177 {
178 let mut mapper = typewriter_python::PythonMapper::new();
179 if let Some(style) = config.py_file_style() {
180 mapper = mapper.with_file_style(style);
181 }
182 let output_dir = project_root.join(config.py_output_dir());
183 return Ok(vec![render_with_mapper(
184 &mapper,
185 type_def,
186 source_path,
187 language,
188 output_dir,
189 )]);
190 }
191 #[cfg(not(feature = "python"))]
192 {
193 if _skip_unavailable {
194 return Ok(vec![]);
195 }
196 anyhow::bail!("language 'python' is not enabled in this build")
197 }
198 }
199 Language::Go => {
200 #[cfg(feature = "go")]
201 {
202 let mut mapper =
203 typewriter_go::GoMapper::new().with_package_name(config.go_package_name());
204 if let Some(style) = config.go_file_style() {
205 mapper = mapper.with_file_style(style);
206 }
207 let output_dir = project_root.join(config.go_output_dir());
208 return Ok(vec![render_with_mapper(
209 &mapper,
210 type_def,
211 source_path,
212 language,
213 output_dir,
214 )]);
215 }
216 #[cfg(not(feature = "go"))]
217 {
218 if _skip_unavailable {
219 return Ok(vec![]);
220 }
221 anyhow::bail!("language 'go' is not enabled in this build")
222 }
223 }
224 Language::Swift => {
225 #[cfg(feature = "swift")]
226 {
227 let mut mapper = typewriter_swift::SwiftMapper::new();
228 if let Some(style) = config.swift_file_style() {
229 mapper = mapper.with_file_style(style);
230 }
231 let output_dir = project_root.join(config.swift_output_dir());
232 return Ok(vec![render_with_mapper(
233 &mapper,
234 type_def,
235 source_path,
236 language,
237 output_dir,
238 )]);
239 }
240 #[cfg(not(feature = "swift"))]
241 {
242 if _skip_unavailable {
243 return Ok(vec![]);
244 }
245 anyhow::bail!("language 'swift' is not enabled in this build")
246 }
247 }
248 Language::Kotlin => {
249 #[cfg(feature = "kotlin")]
250 {
251 let mut mapper = typewriter_kotlin::KotlinMapper::new()
252 .with_package_name(config.kotlin_package_name());
253 if let Some(style) = config.kotlin_file_style() {
254 mapper = mapper.with_file_style(style);
255 }
256 let output_dir = project_root.join(config.kotlin_output_dir());
257 return Ok(vec![render_with_mapper(
258 &mapper,
259 type_def,
260 source_path,
261 language,
262 output_dir,
263 )]);
264 }
265 #[cfg(not(feature = "kotlin"))]
266 {
267 if _skip_unavailable {
268 return Ok(vec![]);
269 }
270 anyhow::bail!("language 'kotlin' is not enabled in this build")
271 }
272 }
273 Language::GraphQL => {
274 #[cfg(feature = "graphql")]
275 {
276 let mut mapper = typewriter_graphql::GraphQLMapper::new();
277 if let Some(style) = config.graphql_file_style() {
278 mapper = mapper.with_file_style(style);
279 }
280 let output_dir = project_root.join(config.graphql_output_dir());
281 return Ok(vec![render_with_mapper(
282 &mapper,
283 type_def,
284 source_path,
285 language,
286 output_dir,
287 )]);
288 }
289 #[cfg(not(feature = "graphql"))]
290 {
291 if _skip_unavailable {
292 return Ok(vec![]);
293 }
294 anyhow::bail!("language 'graphql' is not enabled in this build")
295 }
296 }
297 Language::JsonSchema => {
298 #[cfg(feature = "json_schema")]
299 {
300 let mut mapper = typewriter_json_schema::JsonSchemaMapper::new();
301 if let Some(style) = config.json_schema_file_style() {
302 mapper = mapper.with_file_style(style);
303 }
304 let output_dir = project_root.join(config.json_schema_output_dir());
305 return Ok(vec![render_with_mapper(
306 &mapper,
307 type_def,
308 source_path,
309 language,
310 output_dir,
311 )]);
312 }
313 #[cfg(not(feature = "json_schema"))]
314 {
315 if _skip_unavailable {
316 return Ok(vec![]);
317 }
318 anyhow::bail!("language 'json_schema' is not enabled in this build")
319 }
320 }
321 }
322}
323
324fn render_with_mapper<M: TypeMapper>(
325 mapper: &M,
326 type_def: &TypeDef,
327 source_path: PathBuf,
328 language: Language,
329 output_dir: PathBuf,
330) -> GeneratedFile {
331 let filename = mapper.output_filename(type_def.name());
332 let content = mapper.emit_type_def(type_def);
333
334 GeneratedFile {
335 type_name: type_def.name().to_string(),
336 language,
337 output_path: output_dir.join(filename),
338 content,
339 source_path,
340 }
341}