1pub mod codegen;
54pub mod config;
55pub mod error;
56pub mod parser;
57
58use std::collections::HashSet;
59use std::path::{Path, PathBuf};
60
61use tracing::{debug, info};
62
63pub use config::CodegenConfig;
64pub use error::{CodegenError, Result};
65
66pub fn generate(config: &CodegenConfig) -> Result<()> {
68 info!("Parsing schema: {:?}", config.schema_file);
69 let schema_sql = std::fs::read_to_string(&config.schema_file)?;
70 let tables = parser::parse_schema(&schema_sql)?;
71 info!("Found {} tables", tables.len());
72
73 let tables = filter_tables(tables, &config.include_tables, &config.exclude_tables);
74 debug!(
75 "After filtering: {} tables (include={}, exclude={})",
76 tables.len(),
77 config.include_tables,
78 config.exclude_tables
79 );
80
81 if config.generate_structs {
82 info!("Generating structs in {:?}", config.output_structs_dir);
83 codegen::generate_structs(&tables, config)?;
84 }
85 if config.generate_dao {
86 info!("Generating DAOs in {:?}", config.output_dao_dir);
87 codegen::generate_daos(&tables, config)?;
88 }
89
90 info!("Code generation complete");
91 Ok(())
92}
93
94fn filter_tables(
96 tables: Vec<parser::TableMetadata>,
97 include: &str,
98 exclude: &str,
99) -> Vec<parser::TableMetadata> {
100 let include_all = include.trim() == "*" || include.trim().is_empty();
101 let include_set: HashSet<String> = if include_all {
102 HashSet::new()
103 } else {
104 include.split(',').map(|s| s.trim().to_string()).collect()
105 };
106 let exclude_set: HashSet<String> = exclude
107 .split(',')
108 .map(|s| s.trim().to_string())
109 .filter(|s| !s.is_empty())
110 .collect();
111
112 tables
113 .into_iter()
114 .filter(|t| {
115 let name = &t.name;
116 let included = include_all || include_set.contains(name);
117 let excluded = exclude_set.contains(name);
118 included && !excluded
119 })
120 .collect()
121}
122
123pub struct CodegenBuilder {
125 config: CodegenConfig,
126}
127
128impl CodegenBuilder {
129 pub fn new(schema_file: impl AsRef<Path>) -> Self {
131 Self {
132 config: CodegenConfig::default_with_schema(schema_file.as_ref().to_path_buf()),
133 }
134 }
135
136 pub fn output_dir(mut self, dir: impl AsRef<Path>) -> Self {
138 let dir = dir.as_ref();
139 self.config.output_structs_dir = dir.join("models");
140 self.config.output_dao_dir = dir.join("dao");
141 self
142 }
143
144 pub fn output_structs_dir(mut self, dir: impl AsRef<Path>) -> Self {
146 self.config.output_structs_dir = dir.as_ref().to_path_buf();
147 self
148 }
149
150 pub fn output_dao_dir(mut self, dir: impl AsRef<Path>) -> Self {
152 self.config.output_dao_dir = dir.as_ref().to_path_buf();
153 self
154 }
155
156 pub fn include_tables(mut self, tables: &[&str]) -> Self {
158 self.config.include_tables = tables.join(",");
159 self
160 }
161
162 pub fn exclude_tables(mut self, tables: &[&str]) -> Self {
164 self.config.exclude_tables = tables.join(",");
165 self
166 }
167
168 pub fn structs_only(mut self) -> Self {
170 self.config.generate_dao = false;
171 self
172 }
173
174 pub fn dao_only(mut self) -> Self {
176 self.config.generate_structs = false;
177 self
178 }
179
180 pub fn models_module(mut self, name: &str) -> Self {
182 self.config.models_module = name.to_string();
183 self
184 }
185
186 pub fn dao_module(mut self, name: &str) -> Self {
188 self.config.dao_module = name.to_string();
189 self
190 }
191
192 pub fn dry_run(mut self) -> Self {
194 self.config.dry_run = true;
195 self
196 }
197
198 pub fn generate(self) -> Result<()> {
200 generate(&self.config)
201 }
202}
203
204#[derive(Debug, Clone, Default, serde::Deserialize)]
206struct CargoMetadataConfig {
207 schema_file: Option<String>,
209
210 #[serde(default)]
212 include_tables: Vec<String>,
213
214 #[serde(default)]
216 exclude_tables: Vec<String>,
217
218 generate_structs: Option<bool>,
220
221 generate_dao: Option<bool>,
223
224 output_structs_dir: Option<String>,
226
227 output_dao_dir: Option<String>,
229
230 models_module: Option<String>,
232
233 dao_module: 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(structs_dir) = metadata_config.output_structs_dir {
320 builder = builder.output_structs_dir(PathBuf::from(&manifest_dir).join(structs_dir));
321 } else {
322 builder = builder.output_structs_dir(out_dir.join("models"));
323 }
324
325 if let Some(dao_dir) = metadata_config.output_dao_dir {
326 builder = builder.output_dao_dir(PathBuf::from(&manifest_dir).join(dao_dir));
327 } else {
328 builder = builder.output_dao_dir(out_dir.join("dao"));
329 }
330
331 if !metadata_config.include_tables.is_empty() {
333 let tables: Vec<&str> = metadata_config
334 .include_tables
335 .iter()
336 .map(|s| s.as_str())
337 .collect();
338 builder = builder.include_tables(&tables);
339 }
340 if !metadata_config.exclude_tables.is_empty() {
341 let tables: Vec<&str> = metadata_config
342 .exclude_tables
343 .iter()
344 .map(|s| s.as_str())
345 .collect();
346 builder = builder.exclude_tables(&tables);
347 }
348
349 if let Some(false) = metadata_config.generate_structs {
351 builder = builder.dao_only();
352 }
353 if let Some(false) = metadata_config.generate_dao {
354 builder = builder.structs_only();
355 }
356
357 if let Some(module) = metadata_config.models_module {
359 builder = builder.models_module(&module);
360 }
361 if let Some(module) = metadata_config.dao_module {
362 builder = builder.dao_module(&module);
363 }
364
365 println!("cargo:rerun-if-changed={}", schema_path.display());
367 println!("cargo:rerun-if-changed={}", cargo_toml_path.display());
368
369 builder.generate()
370}