1pub mod codegen;
60pub mod config;
61pub mod error;
62pub mod parser;
63
64use std::collections::HashSet;
65use std::path::{Path, PathBuf};
66
67use tracing::{debug, info};
68
69pub use config::CodegenConfig;
70pub use error::{CodegenError, Result};
71
72pub fn generate(config: &CodegenConfig) -> Result<()> {
74 info!("Parsing schema: {:?}", config.schema_file);
75 let schema_sql = std::fs::read_to_string(&config.schema_file)?;
76 let tables = parser::parse_schema(&schema_sql)?;
77 info!("Found {} tables", tables.len());
78
79 let tables = filter_tables(tables, &config.include_tables, &config.exclude_tables);
80 debug!(
81 "After filtering: {} tables (include={}, exclude={})",
82 tables.len(),
83 config.include_tables,
84 config.exclude_tables
85 );
86
87 if config.generate_structs {
88 info!("Generating structs in {:?}", config.output_structs_dir);
89 codegen::generate_structs(&tables, config)?;
90 }
91 if config.generate_dao {
92 info!("Generating DAOs in {:?}", config.output_dao_dir);
93 codegen::generate_daos(&tables, config)?;
94 }
95
96 if config.generate_structs && config.generate_dao {
98 if let (Some(structs_parent), Some(dao_parent)) = (
99 config.output_structs_dir.parent(),
100 config.output_dao_dir.parent(),
101 ) {
102 if structs_parent == dao_parent {
103 let structs_dir_name = config
104 .output_structs_dir
105 .file_name()
106 .and_then(|n| n.to_str())
107 .unwrap_or("models");
108 let dao_dir_name = config
109 .output_dao_dir
110 .file_name()
111 .and_then(|n| n.to_str())
112 .unwrap_or("dao");
113
114 let parent_mod_path = structs_parent.join("mod.rs");
115 let parent_mod_content = format!(
116 "// Generated by rdbi-codegen - do not edit manually\n#![allow(dead_code)]\n\npub mod {};\npub mod {};\n",
117 dao_dir_name, structs_dir_name
118 );
119
120 if !config.dry_run {
121 std::fs::write(&parent_mod_path, parent_mod_content)?;
122 info!("Generated parent mod.rs at {:?}", parent_mod_path);
123 }
124 }
125 }
126 }
127
128 info!("Code generation complete");
129 Ok(())
130}
131
132fn filter_tables(
134 tables: Vec<parser::TableMetadata>,
135 include: &str,
136 exclude: &str,
137) -> Vec<parser::TableMetadata> {
138 let include_all = include.trim() == "*" || include.trim().is_empty();
139 let include_set: HashSet<String> = if include_all {
140 HashSet::new()
141 } else {
142 include.split(',').map(|s| s.trim().to_string()).collect()
143 };
144 let exclude_set: HashSet<String> = exclude
145 .split(',')
146 .map(|s| s.trim().to_string())
147 .filter(|s| !s.is_empty())
148 .collect();
149
150 tables
151 .into_iter()
152 .filter(|t| {
153 let name = &t.name;
154 let included = include_all || include_set.contains(name);
155 let excluded = exclude_set.contains(name);
156 included && !excluded
157 })
158 .collect()
159}
160
161pub struct CodegenBuilder {
163 config: CodegenConfig,
164}
165
166impl CodegenBuilder {
167 pub fn new(schema_file: impl AsRef<Path>) -> Self {
169 Self {
170 config: CodegenConfig::default_with_schema(schema_file.as_ref().to_path_buf()),
171 }
172 }
173
174 pub fn output_dir(mut self, dir: impl AsRef<Path>) -> Self {
176 let dir = dir.as_ref();
177 self.config.output_structs_dir = dir.join("models");
178 self.config.output_dao_dir = dir.join("dao");
179 self
180 }
181
182 pub fn output_structs_dir(mut self, dir: impl AsRef<Path>) -> Self {
184 self.config.output_structs_dir = dir.as_ref().to_path_buf();
185 self
186 }
187
188 pub fn output_dao_dir(mut self, dir: impl AsRef<Path>) -> Self {
190 self.config.output_dao_dir = dir.as_ref().to_path_buf();
191 self
192 }
193
194 pub fn include_tables(mut self, tables: &[&str]) -> Self {
196 self.config.include_tables = tables.join(",");
197 self
198 }
199
200 pub fn exclude_tables(mut self, tables: &[&str]) -> Self {
202 self.config.exclude_tables = tables.join(",");
203 self
204 }
205
206 pub fn structs_only(mut self) -> Self {
208 self.config.generate_dao = false;
209 self
210 }
211
212 pub fn dao_only(mut self) -> Self {
214 self.config.generate_structs = false;
215 self
216 }
217
218 pub fn models_module(mut self, name: &str) -> Self {
220 self.config.models_module = name.to_string();
221 self
222 }
223
224 pub fn dao_module(mut self, name: &str) -> Self {
226 self.config.dao_module = name.to_string();
227 self
228 }
229
230 pub fn dry_run(mut self) -> Self {
232 self.config.dry_run = true;
233 self
234 }
235
236 pub fn generate(self) -> Result<()> {
238 generate(&self.config)
239 }
240}
241
242#[derive(Debug, Clone, Default, serde::Deserialize)]
244struct CargoMetadataConfig {
245 schema_file: Option<String>,
247
248 #[serde(default)]
250 include_tables: Vec<String>,
251
252 #[serde(default)]
254 exclude_tables: Vec<String>,
255
256 generate_structs: Option<bool>,
258
259 generate_dao: Option<bool>,
261
262 output_structs_dir: Option<String>,
264
265 output_dao_dir: Option<String>,
267}
268
269#[derive(Debug, serde::Deserialize)]
270struct CargoToml {
271 package: Option<CargoPackage>,
272}
273
274#[derive(Debug, serde::Deserialize)]
275struct CargoPackage {
276 metadata: Option<CargoPackageMetadata>,
277}
278
279#[derive(Debug, serde::Deserialize)]
280struct CargoPackageMetadata {
281 #[serde(rename = "rdbi-codegen")]
282 rdbi_codegen: Option<CargoMetadataConfig>,
283}
284
285pub fn generate_from_cargo_metadata() -> Result<()> {
307 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
308 CodegenError::ConfigError(
309 "CARGO_MANIFEST_DIR not set - are you running from build.rs?".into(),
310 )
311 })?;
312
313 let cargo_toml_path = PathBuf::from(&manifest_dir).join("Cargo.toml");
314 let cargo_toml_content = std::fs::read_to_string(&cargo_toml_path)?;
315
316 let cargo_toml: CargoToml = toml::from_str(&cargo_toml_content).map_err(|e| {
317 CodegenError::ConfigError(format!(
318 "Failed to parse {}: {}",
319 cargo_toml_path.display(),
320 e
321 ))
322 })?;
323
324 let metadata_config = cargo_toml
325 .package
326 .and_then(|p| p.metadata)
327 .and_then(|m| m.rdbi_codegen)
328 .ok_or_else(|| {
329 CodegenError::ConfigError(
330 "Missing [package.metadata.rdbi-codegen] section in Cargo.toml".into(),
331 )
332 })?;
333
334 let schema_file = metadata_config.schema_file.ok_or_else(|| {
335 CodegenError::ConfigError(
336 "schema_file is required in [package.metadata.rdbi-codegen]".into(),
337 )
338 })?;
339
340 let schema_path = PathBuf::from(&manifest_dir).join(&schema_file);
342
343 let out_dir = std::env::var("OUT_DIR").map(PathBuf::from).map_err(|_| {
345 CodegenError::ConfigError("OUT_DIR not set - are you running from build.rs?".into())
346 })?;
347
348 let mut builder = CodegenBuilder::new(&schema_path);
349
350 if let Some(ref structs_dir) = metadata_config.output_structs_dir {
352 builder = builder.output_structs_dir(PathBuf::from(&manifest_dir).join(structs_dir));
353
354 let module_path = structs_dir
357 .strip_prefix("src/")
358 .unwrap_or(structs_dir)
359 .replace('/', "::");
360 builder = builder.models_module(&module_path);
361 } else {
362 builder = builder.output_structs_dir(out_dir.join("models"));
363 }
364
365 if let Some(ref dao_dir) = metadata_config.output_dao_dir {
366 builder = builder.output_dao_dir(PathBuf::from(&manifest_dir).join(dao_dir));
367
368 let module_path = dao_dir
371 .strip_prefix("src/")
372 .unwrap_or(dao_dir)
373 .replace('/', "::");
374 builder = builder.dao_module(&module_path);
375 } else {
376 builder = builder.output_dao_dir(out_dir.join("dao"));
377 }
378
379 if !metadata_config.include_tables.is_empty() {
381 let tables: Vec<&str> = metadata_config
382 .include_tables
383 .iter()
384 .map(|s| s.as_str())
385 .collect();
386 builder = builder.include_tables(&tables);
387 }
388 if !metadata_config.exclude_tables.is_empty() {
389 let tables: Vec<&str> = metadata_config
390 .exclude_tables
391 .iter()
392 .map(|s| s.as_str())
393 .collect();
394 builder = builder.exclude_tables(&tables);
395 }
396
397 if let Some(false) = metadata_config.generate_structs {
399 builder = builder.dao_only();
400 }
401 if let Some(false) = metadata_config.generate_dao {
402 builder = builder.structs_only();
403 }
404
405 println!("cargo:rerun-if-changed={}", schema_path.display());
407 println!("cargo:rerun-if-changed={}", cargo_toml_path.display());
408
409 builder.generate()
410}