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