1pub mod codegen;
61pub mod config;
62pub mod error;
63pub mod parser;
64
65use std::collections::HashSet;
66use std::path::{Path, PathBuf};
67
68use tracing::{debug, info};
69
70pub use config::CodegenConfig;
71pub use error::{CodegenError, Result};
72
73pub fn generate(config: &CodegenConfig) -> Result<()> {
75 info!("Parsing schema: {:?}", config.schema_file);
76 let schema_sql = std::fs::read_to_string(&config.schema_file)?;
77 let tables = parser::parse_schema(&schema_sql)?;
78 info!("Found {} tables", tables.len());
79
80 let tables = filter_tables(tables, &config.include_tables, &config.exclude_tables);
81 debug!(
82 "After filtering: {} tables (include={}, exclude={})",
83 tables.len(),
84 config.include_tables,
85 config.exclude_tables
86 );
87
88 if config.generate_structs {
89 info!("Generating structs in {:?}", config.output_structs_dir);
90 codegen::generate_structs(&tables, config)?;
91 }
92 if config.generate_dao {
93 info!("Generating DAOs in {:?}", config.output_dao_dir);
94 codegen::generate_daos(&tables, config)?;
95 }
96
97 info!("Code generation complete");
98 Ok(())
99}
100
101fn filter_tables(
103 tables: Vec<parser::TableMetadata>,
104 include: &str,
105 exclude: &str,
106) -> Vec<parser::TableMetadata> {
107 let include_all = include.trim() == "*" || include.trim().is_empty();
108 let include_set: HashSet<String> = if include_all {
109 HashSet::new()
110 } else {
111 include.split(',').map(|s| s.trim().to_string()).collect()
112 };
113 let exclude_set: HashSet<String> = exclude
114 .split(',')
115 .map(|s| s.trim().to_string())
116 .filter(|s| !s.is_empty())
117 .collect();
118
119 tables
120 .into_iter()
121 .filter(|t| {
122 let name = &t.name;
123 let included = include_all || include_set.contains(name);
124 let excluded = exclude_set.contains(name);
125 included && !excluded
126 })
127 .collect()
128}
129
130pub struct CodegenBuilder {
132 config: CodegenConfig,
133}
134
135impl CodegenBuilder {
136 pub fn new(schema_file: impl AsRef<Path>) -> Self {
138 Self {
139 config: CodegenConfig::default_with_schema(schema_file.as_ref().to_path_buf()),
140 }
141 }
142
143 pub fn output_dir(mut self, dir: impl AsRef<Path>) -> Self {
145 let dir = dir.as_ref();
146 self.config.output_structs_dir = dir.join("models");
147 self.config.output_dao_dir = dir.join("dao");
148 self
149 }
150
151 pub fn output_structs_dir(mut self, dir: impl AsRef<Path>) -> Self {
153 self.config.output_structs_dir = dir.as_ref().to_path_buf();
154 self
155 }
156
157 pub fn output_dao_dir(mut self, dir: impl AsRef<Path>) -> Self {
159 self.config.output_dao_dir = dir.as_ref().to_path_buf();
160 self
161 }
162
163 pub fn include_tables(mut self, tables: &[&str]) -> Self {
165 self.config.include_tables = tables.join(",");
166 self
167 }
168
169 pub fn exclude_tables(mut self, tables: &[&str]) -> Self {
171 self.config.exclude_tables = tables.join(",");
172 self
173 }
174
175 pub fn structs_only(mut self) -> Self {
177 self.config.generate_dao = false;
178 self
179 }
180
181 pub fn dao_only(mut self) -> Self {
183 self.config.generate_structs = false;
184 self
185 }
186
187 pub fn models_module(mut self, name: &str) -> Self {
189 self.config.models_module = name.to_string();
190 self
191 }
192
193 pub fn dao_module(mut self, name: &str) -> Self {
195 self.config.dao_module = name.to_string();
196 self
197 }
198
199 pub fn dry_run(mut self) -> Self {
201 self.config.dry_run = true;
202 self
203 }
204
205 pub fn generate(self) -> Result<()> {
207 generate(&self.config)
208 }
209}
210
211#[derive(Debug, Clone, Default, serde::Deserialize)]
213struct CargoMetadataConfig {
214 schema_file: Option<String>,
216
217 #[serde(default)]
219 include_tables: Vec<String>,
220
221 #[serde(default)]
223 exclude_tables: Vec<String>,
224
225 generate_structs: Option<bool>,
227
228 generate_dao: Option<bool>,
230
231 output_structs_dir: Option<String>,
233
234 output_dao_dir: Option<String>,
236
237 models_module: Option<String>,
239
240 dao_module: Option<String>,
242}
243
244#[derive(Debug, serde::Deserialize)]
245struct CargoToml {
246 package: Option<CargoPackage>,
247}
248
249#[derive(Debug, serde::Deserialize)]
250struct CargoPackage {
251 metadata: Option<CargoPackageMetadata>,
252}
253
254#[derive(Debug, serde::Deserialize)]
255struct CargoPackageMetadata {
256 #[serde(rename = "rdbi-codegen")]
257 rdbi_codegen: Option<CargoMetadataConfig>,
258}
259
260pub fn generate_from_cargo_metadata() -> Result<()> {
282 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
283 CodegenError::ConfigError(
284 "CARGO_MANIFEST_DIR not set - are you running from build.rs?".into(),
285 )
286 })?;
287
288 let cargo_toml_path = PathBuf::from(&manifest_dir).join("Cargo.toml");
289 let cargo_toml_content = std::fs::read_to_string(&cargo_toml_path)?;
290
291 let cargo_toml: CargoToml = toml::from_str(&cargo_toml_content).map_err(|e| {
292 CodegenError::ConfigError(format!(
293 "Failed to parse {}: {}",
294 cargo_toml_path.display(),
295 e
296 ))
297 })?;
298
299 let metadata_config = cargo_toml
300 .package
301 .and_then(|p| p.metadata)
302 .and_then(|m| m.rdbi_codegen)
303 .ok_or_else(|| {
304 CodegenError::ConfigError(
305 "Missing [package.metadata.rdbi-codegen] section in Cargo.toml".into(),
306 )
307 })?;
308
309 let schema_file = metadata_config.schema_file.ok_or_else(|| {
310 CodegenError::ConfigError(
311 "schema_file is required in [package.metadata.rdbi-codegen]".into(),
312 )
313 })?;
314
315 let schema_path = PathBuf::from(&manifest_dir).join(&schema_file);
317
318 let out_dir = std::env::var("OUT_DIR").map(PathBuf::from).map_err(|_| {
320 CodegenError::ConfigError("OUT_DIR not set - are you running from build.rs?".into())
321 })?;
322
323 let mut builder = CodegenBuilder::new(&schema_path);
324
325 if let Some(structs_dir) = metadata_config.output_structs_dir {
327 builder = builder.output_structs_dir(PathBuf::from(&manifest_dir).join(structs_dir));
328 } else {
329 builder = builder.output_structs_dir(out_dir.join("models"));
330 }
331
332 if let Some(dao_dir) = metadata_config.output_dao_dir {
333 builder = builder.output_dao_dir(PathBuf::from(&manifest_dir).join(dao_dir));
334 } else {
335 builder = builder.output_dao_dir(out_dir.join("dao"));
336 }
337
338 if !metadata_config.include_tables.is_empty() {
340 let tables: Vec<&str> = metadata_config
341 .include_tables
342 .iter()
343 .map(|s| s.as_str())
344 .collect();
345 builder = builder.include_tables(&tables);
346 }
347 if !metadata_config.exclude_tables.is_empty() {
348 let tables: Vec<&str> = metadata_config
349 .exclude_tables
350 .iter()
351 .map(|s| s.as_str())
352 .collect();
353 builder = builder.exclude_tables(&tables);
354 }
355
356 if let Some(false) = metadata_config.generate_structs {
358 builder = builder.dao_only();
359 }
360 if let Some(false) = metadata_config.generate_dao {
361 builder = builder.structs_only();
362 }
363
364 if let Some(module) = metadata_config.models_module {
366 builder = builder.models_module(&module);
367 }
368 if let Some(module) = metadata_config.dao_module {
369 builder = builder.dao_module(&module);
370 }
371
372 println!("cargo:rerun-if-changed={}", schema_path.display());
374 println!("cargo:rerun-if-changed={}", cargo_toml_path.display());
375
376 builder.generate()
377}