use console::style;
use std::fs;
use std::path::Path;
use crate::templates;
pub fn run(name: String) {
let struct_name = to_pascal_case(&name);
let struct_name = if struct_name.ends_with("Job") {
struct_name
} else {
format!("{struct_name}Job")
};
let file_name = to_snake_case(&struct_name);
if !is_valid_identifier(&file_name) {
eprintln!(
"{} '{}' is not a valid job name",
style("Error:").red().bold(),
name
);
std::process::exit(1);
}
let jobs_dir = Path::new("src/jobs");
let job_file = jobs_dir.join(format!("{file_name}.rs"));
let mod_file = jobs_dir.join("mod.rs");
if !Path::new("src").exists() {
eprintln!(
"{} Not in a Ferro project root directory",
style("Error:").red().bold()
);
eprintln!(
"{}",
style("Make sure you're in a Ferro project directory with a src/ folder.").dim()
);
std::process::exit(1);
}
if !jobs_dir.exists() {
if let Err(e) = fs::create_dir_all(jobs_dir) {
eprintln!(
"{} Failed to create jobs directory: {}",
style("Error:").red().bold(),
e
);
std::process::exit(1);
}
println!("{} Created src/jobs/", style("✓").green());
let mod_content = templates::jobs_mod();
if let Err(e) = fs::write(&mod_file, mod_content) {
eprintln!(
"{} Failed to create mod.rs: {}",
style("Error:").red().bold(),
e
);
std::process::exit(1);
}
println!("{} Created src/jobs/mod.rs", style("✓").green());
}
if job_file.exists() {
eprintln!(
"{} Job '{}' already exists at {}",
style("Info:").yellow().bold(),
struct_name,
job_file.display()
);
std::process::exit(0);
}
if mod_file.exists() {
let mod_content = fs::read_to_string(&mod_file).unwrap_or_default();
let mod_decl = format!("mod {file_name};");
let pub_mod_decl = format!("pub mod {file_name};");
if mod_content.contains(&mod_decl) || mod_content.contains(&pub_mod_decl) {
eprintln!(
"{} Module '{}' is already declared in src/jobs/mod.rs",
style("Info:").yellow().bold(),
file_name
);
std::process::exit(0);
}
}
let job_content = templates::job_template(&file_name, &struct_name);
if let Err(e) = fs::write(&job_file, job_content) {
eprintln!(
"{} Failed to write job file: {}",
style("Error:").red().bold(),
e
);
std::process::exit(1);
}
println!("{} Created {}", style("✓").green(), job_file.display());
if let Err(e) = update_mod_file(&mod_file, &file_name, &struct_name) {
eprintln!(
"{} Failed to update mod.rs: {}",
style("Error:").red().bold(),
e
);
std::process::exit(1);
}
println!("{} Updated src/jobs/mod.rs", style("✓").green());
println!();
println!(
"Job {} created successfully!",
style(&struct_name).cyan().bold()
);
println!();
println!("Next steps:");
println!(
" {} Add job data fields and implement handle() in {}",
style("1.").dim(),
job_file.display()
);
println!();
println!(
" {} Add the jobs module to src/lib.rs or src/main.rs:",
style("2.").dim()
);
println!(" {}", style("mod jobs;").cyan());
println!();
println!(" {} Dispatch the job in your code:", style("3.").dim());
println!(
" {}",
style(format!("use crate::jobs::{file_name}::{struct_name};")).cyan()
);
println!(
" {}",
style(format!(
"{struct_name} {{ /* fields */ }}.dispatch().await?;"
))
.cyan()
);
println!();
println!(" {} Register the job with your worker:", style("4.").dim());
println!(
" {}",
style(format!("worker.register::<{struct_name}>();")).cyan()
);
println!();
}
fn is_valid_identifier(name: &str) -> bool {
if name.is_empty() {
return false;
}
let mut chars = name.chars();
match chars.next() {
Some(c) if c.is_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_alphanumeric() || c == '_')
}
fn to_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() {
if i > 0 {
result.push('_');
}
result.push(c.to_lowercase().next().unwrap());
} else {
result.push(c);
}
}
result
}
fn to_pascal_case(s: &str) -> String {
let mut result = String::new();
let mut capitalize_next = true;
for c in s.chars() {
if c == '_' || c == '-' || c == ' ' {
capitalize_next = true;
} else if capitalize_next {
result.push(c.to_uppercase().next().unwrap());
capitalize_next = false;
} else {
result.push(c);
}
}
result
}
fn update_mod_file(mod_file: &Path, file_name: &str, struct_name: &str) -> Result<(), String> {
let content =
fs::read_to_string(mod_file).map_err(|e| format!("Failed to read mod.rs: {e}"))?;
let pub_mod_decl = format!("pub mod {file_name};");
let pub_use_decl = format!("pub use {file_name}::{struct_name};");
let lines: Vec<&str> = content.lines().collect();
let mut last_pub_mod_idx = None;
let mut last_pub_use_idx = None;
for (i, line) in lines.iter().enumerate() {
if line.trim().starts_with("pub mod ") {
last_pub_mod_idx = Some(i);
}
if line.trim().starts_with("pub use ") {
last_pub_use_idx = Some(i);
}
}
let mut new_lines: Vec<String> = Vec::new();
if let Some(idx) = last_pub_mod_idx {
for (i, line) in lines.iter().enumerate() {
new_lines.push(line.to_string());
if i == idx {
new_lines.push(pub_mod_decl.clone());
}
}
} else {
let mut content_end = lines.len();
while content_end > 0 && lines[content_end - 1].trim().is_empty() {
content_end -= 1;
}
for (i, line) in lines.iter().enumerate() {
new_lines.push(line.to_string());
if i == content_end.saturating_sub(1) || (content_end == 0 && i == 0) {
new_lines.push(pub_mod_decl.clone());
}
}
if lines.is_empty() {
new_lines.push(pub_mod_decl.clone());
}
}
if last_pub_use_idx.is_some() {
let mut insert_idx = None;
for (i, line) in new_lines.iter().enumerate() {
if line.trim().starts_with("pub use ") {
insert_idx = Some(i);
}
}
if let Some(idx) = insert_idx {
new_lines.insert(idx + 1, pub_use_decl);
}
}
let new_content = new_lines.join("\n");
let new_content = if new_content.ends_with('\n') {
new_content
} else {
format!("{new_content}\n")
};
fs::write(mod_file, new_content).map_err(|e| format!("Failed to write mod.rs: {e}"))?;
Ok(())
}