ferro_cli/commands/
make_job.rs1use console::style;
4use std::fs;
5use std::path::Path;
6
7use crate::templates;
8
9pub fn run(name: String) {
10 let struct_name = to_pascal_case(&name);
12
13 let struct_name = if struct_name.ends_with("Job") {
15 struct_name
16 } else {
17 format!("{struct_name}Job")
18 };
19
20 let file_name = to_snake_case(&struct_name);
22
23 if !is_valid_identifier(&file_name) {
25 eprintln!(
26 "{} '{}' is not a valid job name",
27 style("Error:").red().bold(),
28 name
29 );
30 std::process::exit(1);
31 }
32
33 let jobs_dir = Path::new("src/jobs");
34 let job_file = jobs_dir.join(format!("{file_name}.rs"));
35 let mod_file = jobs_dir.join("mod.rs");
36
37 if !Path::new("src").exists() {
39 eprintln!(
40 "{} Not in a Ferro project root directory",
41 style("Error:").red().bold()
42 );
43 eprintln!(
44 "{}",
45 style("Make sure you're in a Ferro project directory with a src/ folder.").dim()
46 );
47 std::process::exit(1);
48 }
49
50 if !jobs_dir.exists() {
52 if let Err(e) = fs::create_dir_all(jobs_dir) {
53 eprintln!(
54 "{} Failed to create jobs directory: {}",
55 style("Error:").red().bold(),
56 e
57 );
58 std::process::exit(1);
59 }
60 println!("{} Created src/jobs/", style("✓").green());
61
62 let mod_content = templates::jobs_mod();
64 if let Err(e) = fs::write(&mod_file, mod_content) {
65 eprintln!(
66 "{} Failed to create mod.rs: {}",
67 style("Error:").red().bold(),
68 e
69 );
70 std::process::exit(1);
71 }
72 println!("{} Created src/jobs/mod.rs", style("✓").green());
73 }
74
75 if job_file.exists() {
77 eprintln!(
78 "{} Job '{}' already exists at {}",
79 style("Info:").yellow().bold(),
80 struct_name,
81 job_file.display()
82 );
83 std::process::exit(0);
84 }
85
86 if mod_file.exists() {
88 let mod_content = fs::read_to_string(&mod_file).unwrap_or_default();
89 let mod_decl = format!("mod {file_name};");
90 let pub_mod_decl = format!("pub mod {file_name};");
91 if mod_content.contains(&mod_decl) || mod_content.contains(&pub_mod_decl) {
92 eprintln!(
93 "{} Module '{}' is already declared in src/jobs/mod.rs",
94 style("Info:").yellow().bold(),
95 file_name
96 );
97 std::process::exit(0);
98 }
99 }
100
101 let job_content = templates::job_template(&file_name, &struct_name);
103
104 if let Err(e) = fs::write(&job_file, job_content) {
106 eprintln!(
107 "{} Failed to write job file: {}",
108 style("Error:").red().bold(),
109 e
110 );
111 std::process::exit(1);
112 }
113 println!("{} Created {}", style("✓").green(), job_file.display());
114
115 if let Err(e) = update_mod_file(&mod_file, &file_name, &struct_name) {
117 eprintln!(
118 "{} Failed to update mod.rs: {}",
119 style("Error:").red().bold(),
120 e
121 );
122 std::process::exit(1);
123 }
124 println!("{} Updated src/jobs/mod.rs", style("✓").green());
125
126 println!();
127 println!(
128 "Job {} created successfully!",
129 style(&struct_name).cyan().bold()
130 );
131 println!();
132 println!("Next steps:");
133 println!(
134 " {} Add job data fields and implement handle() in {}",
135 style("1.").dim(),
136 job_file.display()
137 );
138 println!();
139 println!(
140 " {} Add the jobs module to src/lib.rs or src/main.rs:",
141 style("2.").dim()
142 );
143 println!(" {}", style("mod jobs;").cyan());
144 println!();
145 println!(" {} Dispatch the job in your code:", style("3.").dim());
146 println!(
147 " {}",
148 style(format!("use crate::jobs::{file_name}::{struct_name};")).cyan()
149 );
150 println!(
151 " {}",
152 style(format!(
153 "{struct_name} {{ /* fields */ }}.dispatch().await?;"
154 ))
155 .cyan()
156 );
157 println!();
158 println!(" {} Register the job with your worker:", style("4.").dim());
159 println!(
160 " {}",
161 style(format!("worker.register::<{struct_name}>();")).cyan()
162 );
163 println!();
164}
165
166fn is_valid_identifier(name: &str) -> bool {
167 if name.is_empty() {
168 return false;
169 }
170
171 let mut chars = name.chars();
172
173 match chars.next() {
175 Some(c) if c.is_alphabetic() || c == '_' => {}
176 _ => return false,
177 }
178
179 chars.all(|c| c.is_alphanumeric() || c == '_')
181}
182
183fn to_snake_case(s: &str) -> String {
184 let mut result = String::new();
185 for (i, c) in s.chars().enumerate() {
186 if c.is_uppercase() {
187 if i > 0 {
188 result.push('_');
189 }
190 result.push(c.to_lowercase().next().unwrap());
191 } else {
192 result.push(c);
193 }
194 }
195 result
196}
197
198fn to_pascal_case(s: &str) -> String {
199 let mut result = String::new();
200 let mut capitalize_next = true;
201
202 for c in s.chars() {
203 if c == '_' || c == '-' || c == ' ' {
204 capitalize_next = true;
205 } else if capitalize_next {
206 result.push(c.to_uppercase().next().unwrap());
207 capitalize_next = false;
208 } else {
209 result.push(c);
210 }
211 }
212 result
213}
214
215fn update_mod_file(mod_file: &Path, file_name: &str, struct_name: &str) -> Result<(), String> {
216 let content =
217 fs::read_to_string(mod_file).map_err(|e| format!("Failed to read mod.rs: {e}"))?;
218
219 let pub_mod_decl = format!("pub mod {file_name};");
220 let pub_use_decl = format!("pub use {file_name}::{struct_name};");
221
222 let lines: Vec<&str> = content.lines().collect();
224
225 let mut last_pub_mod_idx = None;
227 let mut last_pub_use_idx = None;
228
229 for (i, line) in lines.iter().enumerate() {
230 if line.trim().starts_with("pub mod ") {
231 last_pub_mod_idx = Some(i);
232 }
233 if line.trim().starts_with("pub use ") {
234 last_pub_use_idx = Some(i);
235 }
236 }
237
238 let mut new_lines: Vec<String> = Vec::new();
240
241 if let Some(idx) = last_pub_mod_idx {
243 for (i, line) in lines.iter().enumerate() {
244 new_lines.push(line.to_string());
245 if i == idx {
246 new_lines.push(pub_mod_decl.clone());
247 }
248 }
249 } else {
250 let mut content_end = lines.len();
252 while content_end > 0 && lines[content_end - 1].trim().is_empty() {
253 content_end -= 1;
254 }
255
256 for (i, line) in lines.iter().enumerate() {
257 new_lines.push(line.to_string());
258 if i == content_end.saturating_sub(1) || (content_end == 0 && i == 0) {
259 new_lines.push(pub_mod_decl.clone());
260 }
261 }
262
263 if lines.is_empty() {
265 new_lines.push(pub_mod_decl.clone());
266 }
267 }
268
269 if last_pub_use_idx.is_some() {
271 let mut insert_idx = None;
273 for (i, line) in new_lines.iter().enumerate() {
274 if line.trim().starts_with("pub use ") {
275 insert_idx = Some(i);
276 }
277 }
278 if let Some(idx) = insert_idx {
279 new_lines.insert(idx + 1, pub_use_decl);
280 }
281 }
282
283 let new_content = new_lines.join("\n");
284
285 let new_content = if new_content.ends_with('\n') {
287 new_content
288 } else {
289 format!("{new_content}\n")
290 };
291
292 fs::write(mod_file, new_content).map_err(|e| format!("Failed to write mod.rs: {e}"))?;
293
294 Ok(())
295}