use rust_ef::error::EFResult;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct ScaffoldColumn {
pub name: String,
pub data_type: String,
pub is_nullable: bool,
pub is_primary_key: bool,
pub max_length: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct ScaffoldTable {
pub name: String,
pub columns: Vec<ScaffoldColumn>,
}
pub fn write_entities(output_dir: &Path, tables: &[ScaffoldTable]) -> EFResult<()> {
std::fs::create_dir_all(output_dir).map_err(io_err)?;
for table in tables {
let type_name = table_name_to_type(&table.name);
let file_name = format!("{}.rs", snake_case(&type_name));
let path = output_dir.join(file_name);
std::fs::write(&path, render_entity(table, &type_name)).map_err(io_err)?;
}
let mod_rs = render_mod_file(tables);
std::fs::write(output_dir.join("mod.rs"), mod_rs).map_err(io_err)?;
Ok(())
}
fn render_mod_file(tables: &[ScaffoldTable]) -> String {
let mut out = String::from("//! Scaffolded entity types (Database First).\n\n");
for table in tables {
let type_name = table_name_to_type(&table.name);
let module = snake_case(&type_name);
out.push_str(&format!("pub mod {module};\n"));
out.push_str(&format!("pub use {module}::{type_name};\n"));
}
out
}
fn render_entity(table: &ScaffoldTable, type_name: &str) -> String {
let mut out = String::from("//! Auto-generated by `rust-ef scaffold dbcontext`.\n\n");
out.push_str("use rust_ef::prelude::*;\n\n");
out.push_str("#[derive(Debug, Clone, EntityType)]\n");
out.push_str(&format!("#[table(\"{}\")]\n", table.name));
out.push_str(&format!("pub struct {type_name} {{\n"));
for col in &table.columns {
let field = column_to_field(&col.name);
let rust_ty = map_rust_type(col);
if col.is_primary_key {
out.push_str(" #[primary_key]\n");
if is_integer_type(&col.data_type) {
out.push_str(" #[auto_increment]\n");
}
}
if !col.is_nullable && !col.is_primary_key {
out.push_str(" #[required]\n");
}
if let Some(n) = col.max_length {
if rust_ty == "String" {
out.push_str(&format!(" #[max_length({n})]\n"));
}
}
out.push_str(&format!(" pub {field}: {rust_ty},\n"));
}
out.push_str("}\n");
out
}
fn table_name_to_type(table: &str) -> String {
let singular = if table.ends_with('s') && table.len() > 1 {
&table[..table.len() - 1]
} else {
table
};
singular
.split('_')
.filter(|p| !p.is_empty())
.map(|p| {
let mut c = p.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
})
.collect()
}
fn snake_case(name: &str) -> String {
let mut out = String::new();
for (i, ch) in name.chars().enumerate() {
if ch.is_uppercase() {
if i > 0 {
out.push('_');
}
out.extend(ch.to_lowercase());
} else {
out.push(ch);
}
}
out
}
fn column_to_field(column: &str) -> String {
column.to_string()
}
fn is_integer_type(sql_type: &str) -> bool {
let t = sql_type.to_ascii_lowercase();
t.contains("int") || t == "serial" || t == "bigserial" || t.contains("serial")
}
fn map_rust_type(col: &ScaffoldColumn) -> String {
let t = col.data_type.to_ascii_lowercase();
let base = if t.contains("bool") {
"bool"
} else if t.contains("bigint") || t == "bigserial" {
"i64"
} else if t.contains("int") || t == "serial" || t.contains("serial") {
"i32"
} else if t.contains("double") || t.contains("decimal") || t.contains("numeric") {
"f64"
} else if t.contains("float") || t == "real" {
"f32"
} else if t.contains("byte") || t == "blob" {
"Vec<u8>"
} else {
"String"
};
if col.is_nullable && base != "String" {
format!("Option<{base}>")
} else if col.is_nullable && base == "String" {
"Option<String>".to_string()
} else {
base.to_string()
}
}
fn io_err(e: std::io::Error) -> rust_ef::error::EFError {
rust_ef::error::EFError::configuration(e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_blog_entity() {
let table = ScaffoldTable {
name: "blogs".into(),
columns: vec![
ScaffoldColumn {
name: "blog_id".into(),
data_type: "integer".into(),
is_nullable: false,
is_primary_key: true,
max_length: None,
},
ScaffoldColumn {
name: "url".into(),
data_type: "character varying".into(),
is_nullable: false,
is_primary_key: false,
max_length: Some(200),
},
],
};
let src = render_entity(&table, "Blog");
assert!(src.contains("struct Blog"));
assert!(src.contains("#[table(\"blogs\")]"));
assert!(src.contains("#[primary_key]"));
assert!(src.contains("#[max_length(200)]"));
}
}