use crate::config::TideConfig;
use crate::utils::print_info;
use colored::Colorize;
use std::fs;
use std::path::Path;
pub async fn list(config_path: &str, verbose: bool) -> Result<(), String> {
let config = TideConfig::load_or_default(config_path)?;
if verbose {
print_info(&format!("Looking for models in: {}", config.paths.models));
}
let models_path = Path::new(&config.paths.models);
if !models_path.exists() {
return Err(format!(
"Models directory not found: {}",
config.paths.models
));
}
let models = scan_models(&config.paths.models)?;
println!("\n{}", "TideORM Models:".cyan().bold());
println!("{}", "─".repeat(80));
if models.is_empty() {
println!(" No models found in {}", config.paths.models);
println!("\n Create a model with:");
println!(
" {}",
"tideorm make model User --fields=\"name:string,email:string:unique\"".yellow()
);
} else {
println!(
" {:<20} {:<30} {:<15} Features",
"Model", "Table", "Fields"
);
println!("{}", "─".repeat(80));
for model in &models {
let features: Vec<&str> = [
if model.has_timestamps {
Some("timestamps")
} else {
None
},
if model.has_soft_deletes {
Some("soft_delete")
} else {
None
},
if model.has_tokenize {
Some("tokenize")
} else {
None
},
if !model.relations.is_empty() {
Some("relations")
} else {
None
},
if !model.translatable.is_empty() {
Some("translatable")
} else {
None
},
]
.into_iter()
.flatten()
.collect();
println!(
" {:<20} {:<30} {:<15} {}",
model.name.green(),
model.table,
model.fields.len().to_string(),
features.join(", ")
);
}
println!("{}", "─".repeat(80));
println!(" Total: {} model(s)", models.len());
}
Ok(())
}
#[derive(Debug)]
struct ModelInfo {
name: String,
table: String,
fields: Vec<String>,
relations: Vec<String>,
translatable: Vec<String>,
has_timestamps: bool,
has_soft_deletes: bool,
has_tokenize: bool,
}
fn scan_models(models_path: &str) -> Result<Vec<ModelInfo>, String> {
let path = Path::new(models_path);
let mut models = Vec::new();
for entry in
fs::read_dir(path).map_err(|e| format!("Failed to read models directory: {}", e))?
{
let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
let file_path = entry.path();
if file_path.extension().is_some_and(|ext| ext == "rs") {
let name = file_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
if name == "mod" {
continue;
}
let content = fs::read_to_string(&file_path)
.map_err(|e| format!("Failed to read model file: {}", e))?;
if let Some(model_info) = parse_model_file(&content) {
models.push(model_info);
}
}
}
models.sort_by(|a, b| a.name.cmp(&b.name));
Ok(models)
}
fn parse_model_file(content: &str) -> Option<ModelInfo> {
let struct_pattern = regex::Regex::new(
r#"(?s)(?:#\[tideorm::model(?:\([^\]]*\))?\]\s*(?:#\[(?:tideorm|index|unique_index)[^\]]*\]\s*)*|#\[derive\([^)]*Model[^)]*\)\]\s*(?:#\[(?:tideorm|index|unique_index)[^\]]*\]\s*)*)pub\s+struct\s+(\w+)"#
).ok()?;
let struct_name = struct_pattern.captures(content)?.get(1)?.as_str();
let table_pattern = regex::Regex::new(
r#"(?:#\[tideorm\([^)]*table\s*=\s*"([^"]+)"[^)]*\)\]|#\[tideorm::model\([^\]]*table\s*=\s*"([^"]+)"[^\]]*\)\])"#
).ok()?;
let table = table_pattern
.captures(content)
.and_then(|c| c.get(1).or_else(|| c.get(2)))
.map(|m| m.as_str().to_string())
.unwrap_or_else(|| crate::utils::pluralize(&crate::utils::to_snake_case(struct_name)));
let field_pattern = regex::Regex::new(r"pub\s+(\w+)\s*:\s*([^,\n}]+)").ok()?;
let fields: Vec<String> = field_pattern
.captures_iter(content)
.map(|c| c.get(1).unwrap().as_str().to_string())
.collect();
let has_timestamps = content.contains("created_at") && content.contains("updated_at");
let has_soft_deletes = content.contains("soft_delete") || content.contains("deleted_at");
let has_tokenize = content.contains("tokenize");
let relation_pattern =
regex::Regex::new(r#"#\[tideorm\([^)]*(?:belongs_to|has_one|has_many)[^)]*\)\]"#).ok()?;
let relations: Vec<String> = relation_pattern
.find_iter(content)
.map(|m| m.as_str().to_string())
.collect();
let translatable_pattern =
regex::Regex::new(r#"#\[tideorm\([^)]*translatable[^)]*\)\]"#).ok()?;
let translatable: Vec<String> = translatable_pattern
.find_iter(content)
.map(|m| m.as_str().to_string())
.collect();
Some(ModelInfo {
name: struct_name.to_string(),
table,
fields,
relations,
translatable,
has_timestamps,
has_soft_deletes,
has_tokenize,
})
}
#[cfg(test)]
mod tests {
use super::parse_model_file;
#[test]
fn test_parse_tideorm_model_attribute() {
let content = r#"
#[tideorm::model(table = "posts")]
#[index("user_id")]
pub struct Post {
pub id: i64,
pub user_id: i64,
pub title: String,
}
"#;
let model = parse_model_file(content).expect("model should parse");
assert_eq!(model.name, "Post");
assert_eq!(model.table, "posts");
assert_eq!(model.fields.len(), 3);
}
}