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 info!("Code generation complete");
97 Ok(())
98}
99
100fn filter_tables(
102 tables: Vec<parser::TableMetadata>,
103 include: &str,
104 exclude: &str,
105) -> Vec<parser::TableMetadata> {
106 let include_all = include.trim() == "*" || include.trim().is_empty();
107 let include_set: HashSet<String> = if include_all {
108 HashSet::new()
109 } else {
110 include.split(',').map(|s| s.trim().to_string()).collect()
111 };
112 let exclude_set: HashSet<String> = exclude
113 .split(',')
114 .map(|s| s.trim().to_string())
115 .filter(|s| !s.is_empty())
116 .collect();
117
118 tables
119 .into_iter()
120 .filter(|t| {
121 let name = &t.name;
122 let included = include_all || include_set.contains(name);
123 let excluded = exclude_set.contains(name);
124 included && !excluded
125 })
126 .collect()
127}
128
129pub struct CodegenBuilder {
131 config: CodegenConfig,
132}
133
134impl CodegenBuilder {
135 pub fn new(schema_file: impl AsRef<Path>) -> Self {
137 Self {
138 config: CodegenConfig::default_with_schema(schema_file.as_ref().to_path_buf()),
139 }
140 }
141
142 pub fn output_dir(mut self, dir: impl AsRef<Path>) -> Self {
144 let dir = dir.as_ref();
145 self.config.output_structs_dir = dir.join("models");
146 self.config.output_dao_dir = dir.join("dao");
147 self
148 }
149
150 pub fn output_structs_dir(mut self, dir: impl AsRef<Path>) -> Self {
152 self.config.output_structs_dir = dir.as_ref().to_path_buf();
153 self
154 }
155
156 pub fn output_dao_dir(mut self, dir: impl AsRef<Path>) -> Self {
158 self.config.output_dao_dir = dir.as_ref().to_path_buf();
159 self
160 }
161
162 pub fn include_tables(mut self, tables: &[&str]) -> Self {
164 self.config.include_tables = tables.join(",");
165 self
166 }
167
168 pub fn exclude_tables(mut self, tables: &[&str]) -> Self {
170 self.config.exclude_tables = tables.join(",");
171 self
172 }
173
174 pub fn structs_only(mut self) -> Self {
176 self.config.generate_dao = false;
177 self
178 }
179
180 pub fn dao_only(mut self) -> Self {
182 self.config.generate_structs = false;
183 self
184 }
185
186 pub fn models_module(mut self, name: &str) -> Self {
188 self.config.models_module = name.to_string();
189 self
190 }
191
192 pub fn dao_module(mut self, name: &str) -> Self {
194 self.config.dao_module = name.to_string();
195 self
196 }
197
198 pub fn dry_run(mut self) -> Self {
200 self.config.dry_run = true;
201 self
202 }
203
204 pub fn generate(self) -> Result<()> {
206 generate(&self.config)
207 }
208}
209
210#[derive(Debug, Clone, Default, serde::Deserialize)]
212struct CargoMetadataConfig {
213 schema_file: Option<String>,
215
216 #[serde(default)]
218 include_tables: Vec<String>,
219
220 #[serde(default)]
222 exclude_tables: Vec<String>,
223
224 generate_structs: Option<bool>,
226
227 generate_dao: Option<bool>,
229
230 output_structs_dir: Option<String>,
232
233 output_dao_dir: Option<String>,
235}
236
237#[derive(Debug, serde::Deserialize)]
238struct CargoToml {
239 package: Option<CargoPackage>,
240}
241
242#[derive(Debug, serde::Deserialize)]
243struct CargoPackage {
244 metadata: Option<CargoPackageMetadata>,
245}
246
247#[derive(Debug, serde::Deserialize)]
248struct CargoPackageMetadata {
249 #[serde(rename = "rdbi-codegen")]
250 rdbi_codegen: Option<CargoMetadataConfig>,
251}
252
253pub fn generate_from_cargo_metadata() -> Result<()> {
275 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
276 CodegenError::ConfigError(
277 "CARGO_MANIFEST_DIR not set - are you running from build.rs?".into(),
278 )
279 })?;
280
281 let cargo_toml_path = PathBuf::from(&manifest_dir).join("Cargo.toml");
282 let cargo_toml_content = std::fs::read_to_string(&cargo_toml_path)?;
283
284 let cargo_toml: CargoToml = toml::from_str(&cargo_toml_content).map_err(|e| {
285 CodegenError::ConfigError(format!(
286 "Failed to parse {}: {}",
287 cargo_toml_path.display(),
288 e
289 ))
290 })?;
291
292 let metadata_config = cargo_toml
293 .package
294 .and_then(|p| p.metadata)
295 .and_then(|m| m.rdbi_codegen)
296 .ok_or_else(|| {
297 CodegenError::ConfigError(
298 "Missing [package.metadata.rdbi-codegen] section in Cargo.toml".into(),
299 )
300 })?;
301
302 let schema_file = metadata_config.schema_file.ok_or_else(|| {
303 CodegenError::ConfigError(
304 "schema_file is required in [package.metadata.rdbi-codegen]".into(),
305 )
306 })?;
307
308 let schema_path = PathBuf::from(&manifest_dir).join(&schema_file);
310
311 let out_dir = std::env::var("OUT_DIR").map(PathBuf::from).map_err(|_| {
313 CodegenError::ConfigError("OUT_DIR not set - are you running from build.rs?".into())
314 })?;
315
316 let mut builder = CodegenBuilder::new(&schema_path);
317
318 if let Some(ref structs_dir) = metadata_config.output_structs_dir {
320 builder = builder.output_structs_dir(PathBuf::from(&manifest_dir).join(structs_dir));
321
322 let module_path = structs_dir
325 .strip_prefix("src/")
326 .unwrap_or(structs_dir)
327 .replace('/', "::");
328 builder = builder.models_module(&module_path);
329 } else {
330 builder = builder.output_structs_dir(out_dir.join("models"));
331 }
332
333 if let Some(ref dao_dir) = metadata_config.output_dao_dir {
334 builder = builder.output_dao_dir(PathBuf::from(&manifest_dir).join(dao_dir));
335
336 let module_path = dao_dir
339 .strip_prefix("src/")
340 .unwrap_or(dao_dir)
341 .replace('/', "::");
342 builder = builder.dao_module(&module_path);
343 } else {
344 builder = builder.output_dao_dir(out_dir.join("dao"));
345 }
346
347 if !metadata_config.include_tables.is_empty() {
349 let tables: Vec<&str> = metadata_config
350 .include_tables
351 .iter()
352 .map(|s| s.as_str())
353 .collect();
354 builder = builder.include_tables(&tables);
355 }
356 if !metadata_config.exclude_tables.is_empty() {
357 let tables: Vec<&str> = metadata_config
358 .exclude_tables
359 .iter()
360 .map(|s| s.as_str())
361 .collect();
362 builder = builder.exclude_tables(&tables);
363 }
364
365 if let Some(false) = metadata_config.generate_structs {
367 builder = builder.dao_only();
368 }
369 if let Some(false) = metadata_config.generate_dao {
370 builder = builder.structs_only();
371 }
372
373 println!("cargo:rerun-if-changed={}", schema_path.display());
375 println!("cargo:rerun-if-changed={}", cargo_toml_path.display());
376
377 builder.generate()
378}