use crate::map_schema::{Column, Table, Schema, Database, Root, Server};
use tokio::sync::Mutex;
pub use crate::config::ConfigClient;
use crate::generator_builder::GeneratorBuilder;
use mssql_client::{Client, Config, IsolationLevel::Serializable, Ready};
use anyhow::{Ok, Result};
const SCHEMA: &str =include_str!("../querys/sql_schema.sql");
pub struct Generator{
client:Mutex<Client<Ready>>,
host:String,
}
impl Generator {
pub async fn new<'a>(cfg: &ConfigClient<'a>)->Result<Self>{
let connect =format!("Server={};User Id={};Password={};TrustServerCertificate=true", cfg.host, cfg.user, cfg.password);
let config= Config::from_connection_string(&connect)?;
let client=Client::connect(config).await?;
Ok(Self{client: Mutex::new(client),
host: cfg.host.to_string(),})
}
pub fn builder<'a>() -> GeneratorBuilder<'a> {
GeneratorBuilder::new()
}
pub async fn get_schema(&self)->Result<Root>{
let mut client = self.client.lock().await;
let rows=client.query(SCHEMA, &[]).await?;
let mut root = Root{
servers:std::collections::BTreeMap::new(),
};
for result in rows{
let row= result?;
let db: String= row.get(0)?;
let schema_name: String= row.get(1)?;
let table_name: String= row.get(2)?;
let column=Column{
name:row.get(3)?,
data_type:row.get(4)?,
};
let server = root.servers.entry(self.host.clone()).or_insert(Server {databases: std::collections::BTreeMap::new(),});
let database=server.databases.entry(db).or_insert(Database { schemas: std::collections::BTreeMap::new(), });
let schema = database.schemas.entry(schema_name).or_insert(Schema { tables: std::collections::BTreeMap::new(), });
let table = schema.tables.entry(table_name).or_insert(Table { columns: Vec::new(), });
table.columns.push(column);
}
let toml = toml::to_string_pretty(&root)?;
std::fs::write("schema.toml", toml)?;
Ok(root)
}
pub fn code_generator(&self, root: &Root)->String{
let mut code = String::new();
code.push_str(r#" // --- NÃO ALTERE ESTE DOCUMENTO MANUALMENTE ---
use std::marker::PhantomData;
pub struct Column<T> {
pub nome: &'static str,
pub _tipo: PhantomData<T>,
}
pub trait ClientSqlExt {
"#
);
let mut count_table=std::collections::HashMap::new();
for server in root.servers.values(){
for database in server.databases.values(){
for schema in database.schemas.values(){
for table in schema.tables.keys(){
let fn_name = Self::sanitize_field_name(table);
*count_table.entry(fn_name).or_insert(0) +=1;
}
}
}
}
let mut processed_tables=std::collections::HashSet::new();
for (_server_name, server) in &root.servers{
for (db_name, database) in &server.databases{
for (schema_name, schema) in &database.schemas{
for table in schema.tables.keys(){
let fn_name= Self::sanitize_field_name(table);
if processed_tables.insert(fn_name.clone()){
let count = count_table.get(&fn_name).unwrap_or(&0);
let full_name = format!("{}_{}_{}",db_name, schema_name, table);
let struct_name =Self::sanitize_struct_name(&full_name);
let fn_name= Self::sanitize_field_name(table);
if *count == 1 {
code.push_str(&format!(
" fn {}(&self) -> Table{} {{ Table{} }}\n",
fn_name, struct_name, struct_name
));
}
}
}
}
}
}
code.push_str("}\n\nimpl ClientSqlExt for crate::ClientSql {}\n\n");
let mut generated_structs = std::collections::HashSet::new();
for (_server_name, server) in &root.servers{
for (db_name, database) in &server.databases{
for (schema_name ,schema) in &database.schemas{
for(table_name, table ) in &schema.tables{
let full_name = format!("{}_{}_{}",db_name, schema_name, table_name);
let struct_name =Self::sanitize_struct_name(&full_name);
if generated_structs.insert(struct_name.clone()) {
let name_field = format!("{}Field", struct_name);
let name_table= format!("Table{}", struct_name);
code.push_str(&format!("pub struct {} {{\n", name_field));
for column in &table.columns{
let rust_type = Self::type_translator(&column.data_type);
let safe_col_name= Self::sanitize_field_name(&column.name);
code.push_str(&format!(" pub {}: Column<{}>,\n", safe_col_name, rust_type));
}
code.push_str("}\n\n");
code.push_str(&format!("pub struct {};\n\n", name_table));
code.push_str(&format!("impl {} {{\n", name_table));
code.push_str(&format!(
" pub fn select<F, Retorno>(self, f: F)\n where F: FnOnce({}) -> Retorno\n {{\n",
name_field
));
code.push_str(&format!(" let fields = {} {{\n", name_field));
for col in &table.columns {
let safe_col_name= Self::sanitize_field_name(&col.name);
code.push_str(&format!(
" {}: Column {{ nome: \"{}\", _tipo: PhantomData }},\n",
safe_col_name, col.name
));
}
code.push_str(" };\n");
code.push_str(" let _selecao = f(fields);\n");
code.push_str(" }\n");
code.push_str("}\n\n");
}
}
}
}
}
code
}
fn sanitize_struct_name(name: &str) -> String {
let clean = Self::remove_accents(name);
let mut result = String::new();
let mut capitalize = true;
for c in clean.chars() {
if c.is_ascii_alphanumeric() {
if capitalize {
result.push(c.to_ascii_uppercase());
capitalize = false;
} else {
result.push(c.to_ascii_lowercase());
}
} else {
capitalize = true;
}
}
if result.is_empty() { return "TableObj".to_string(); }
if result.starts_with(|c: char| c.is_ascii_digit()) { result.insert(0, '_'); }
result
}
fn sanitize_field_name(name: &str) -> String {
let clean = Self::remove_accents(name);
let mut result = String::new();
for c in clean.chars() {
if c.is_ascii_alphanumeric() {
result.push(c.to_ascii_lowercase());
} else {
if !result.ends_with('_') { result.push('_'); }
}
}
let mut final_str = result.trim_matches('_').to_string();
if final_str.is_empty() { final_str = "col".to_string(); }
else if final_str.starts_with(|c: char| c.is_ascii_digit()) { final_str.insert(0, '_'); }
match final_str.as_str() {
"type" | "match" | "let" | "final" | "fn" | "struct" | "enum" | "trait" | "impl" | "where" | "for" | "loop" | "while" | "if" | "else" | "return" | "break" | "continue" | "mut" | "ref" | "pub" | "use" | "mod" | "crate" | "self" | "super" | "as" | "in" | "move" | "const" | "static" | "true" | "false" => {
final_str.push_str("_col");
}
_ => {}
}
final_str
}
fn remove_accents(name: &str) -> String {
name.replace("á", "a").replace("ã", "a").replace("â", "a").replace("à", "a")
.replace("é", "e").replace("ê", "e")
.replace("í", "i")
.replace("ó", "o").replace("õ", "o").replace("ô", "o")
.replace("ú", "u")
.replace("ç", "c")
.replace("Á", "A").replace("Ã", "A").replace("Â", "A").replace("À", "A")
.replace("É", "E").replace("Ê", "E")
.replace("Í", "I")
.replace("Ó", "O").replace("Õ", "O").replace("Ô", "O")
.replace("Ú", "U")
.replace("Ç", "C")
}
pub fn type_translator(sql_type: &str)->&'static str{
let clean_type=sql_type.to_uppercase();
let base = clean_type.split('(').next().unwrap_or("").trim();
match base {
"INT" | "INTEGER" => "Option<i32>",
"BIGINT" => "Option<i64>",
"SMALLINT" => "Option<i16>",
"TINYINT" => "Option<u8>",
"BIT" => "Option<bool>",
"DECIMAL" | "NUMERIC" | "MONEY" => "Option<rust_decimal::Decimal>",
"FLOAT" => "Option<f64>",
"REAL" => "Option<f32>",
"CHAR" | "NCHAR" | "VARCHAR" | "NVARCHAR" | "TEXT" | "NTEXT" | "XML" => "Option<String>",
"DATE" => "Option<chrono::NaiveDate>",
"TIME" => "Option<chrono::NaiveTime>",
"DATETIME" | "DATETIME2" | "SMALLDATETIME" => "Option<chrono::NaiveDateTime>",
"DATETIMEOFFSET" => "Option<chrono::DateTime<chrono::FixedOffset>>",
"UNIQUEIDENTIFIER" => "Option<uuid::Uuid>",
"VARBINARY" | "IMAGE" | "TIMESTAMP" => "Option<Vec<u8>>",
_ => "Option<String>",
}
}
}