ferro_cli/commands/
make_migration.rs1use chrono::Local;
2use console::style;
3use std::fs;
4use std::path::Path;
5
6pub fn run(name: String) {
7 let file_name = to_snake_case(&name);
9
10 if !is_valid_identifier(&file_name) {
12 eprintln!(
13 "{} '{}' is not a valid migration name",
14 style("Error:").red().bold(),
15 name
16 );
17 std::process::exit(1);
18 }
19
20 let table_name = extract_table_name(&file_name);
22 let table_enum_name = to_pascal_case(&table_name);
23
24 let migrations_dir = Path::new("src/migrations");
25
26 if !migrations_dir.exists() {
28 if let Err(e) = fs::create_dir_all(migrations_dir) {
29 eprintln!(
30 "{} Failed to create migrations directory: {}",
31 style("Error:").red().bold(),
32 e
33 );
34 std::process::exit(1);
35 }
36 println!("{} Created src/migrations directory", style("✓").green());
37 }
38
39 let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
41 let migration_file_name = format!("m{timestamp}_{file_name}");
42 let migration_file = migrations_dir.join(format!("{migration_file_name}.rs"));
43 let mod_file = migrations_dir.join("mod.rs");
44
45 if migration_file.exists() {
47 eprintln!(
48 "{} Migration '{}' already exists at {}",
49 style("Info:").yellow().bold(),
50 migration_file_name,
51 migration_file.display()
52 );
53 std::process::exit(0);
54 }
55
56 let migration_content = migration_template(&table_name, &table_enum_name);
58
59 if let Err(e) = fs::write(&migration_file, &migration_content) {
61 eprintln!(
62 "{} Failed to write migration file: {}",
63 style("Error:").red().bold(),
64 e
65 );
66 std::process::exit(1);
67 }
68 println!(
69 "{} Created {}",
70 style("✓").green(),
71 migration_file.display()
72 );
73
74 if mod_file.exists() {
76 if let Err(e) = update_mod_file(&mod_file, &migration_file_name) {
77 eprintln!(
78 "{} Failed to update mod.rs: {}",
79 style("Error:").red().bold(),
80 e
81 );
82 std::process::exit(1);
83 }
84 println!("{} Updated src/migrations/mod.rs", style("✓").green());
85 } else {
86 let mod_content = migrator_mod_template(&migration_file_name);
88 if let Err(e) = fs::write(&mod_file, mod_content) {
89 eprintln!(
90 "{} Failed to create mod.rs: {}",
91 style("Error:").red().bold(),
92 e
93 );
94 std::process::exit(1);
95 }
96 println!("{} Created src/migrations/mod.rs", style("✓").green());
97 }
98
99 println!();
100 println!(
101 "Migration {} created successfully!",
102 style(&migration_file_name).cyan().bold()
103 );
104 println!();
105 println!("Next steps:");
106 println!(
107 " {} Edit the migration file to define your schema",
108 style("1.").dim()
109 );
110 println!(
111 " {} Run {} to apply the migration",
112 style("2.").dim(),
113 style("ferro migrate").cyan()
114 );
115 println!();
116}
117
118fn is_valid_identifier(name: &str) -> bool {
119 if name.is_empty() {
120 return false;
121 }
122
123 let mut chars = name.chars();
124
125 match chars.next() {
127 Some(c) if c.is_alphabetic() || c == '_' => {}
128 _ => return false,
129 }
130
131 chars.all(|c| c.is_alphanumeric() || c == '_')
133}
134
135fn to_snake_case(s: &str) -> String {
136 let mut result = String::new();
137 for (i, c) in s.chars().enumerate() {
138 if c.is_uppercase() {
139 if i > 0 {
140 result.push('_');
141 }
142 result.push(c.to_lowercase().next().unwrap());
143 } else if c == '-' || c == ' ' {
144 result.push('_');
145 } else {
146 result.push(c);
147 }
148 }
149 result
150}
151
152fn to_pascal_case(s: &str) -> String {
153 let mut result = String::new();
154 let mut capitalize_next = true;
155
156 for c in s.chars() {
157 if c == '_' || c == '-' || c == ' ' {
158 capitalize_next = true;
159 } else if capitalize_next {
160 result.push(c.to_uppercase().next().unwrap());
161 capitalize_next = false;
162 } else {
163 result.push(c);
164 }
165 }
166 result
167}
168
169fn extract_table_name(name: &str) -> String {
174 if name.starts_with("create_") && name.ends_with("_table") {
176 let without_prefix = name.strip_prefix("create_").unwrap();
177 let without_suffix = without_prefix.strip_suffix("_table").unwrap();
178 return without_suffix.to_string();
179 }
180
181 if name.contains("_to_") {
182 if let Some(pos) = name.rfind("_to_") {
184 return name[pos + 4..].to_string();
185 }
186 }
187
188 if name.starts_with("drop_") && name.ends_with("_table") {
189 let without_prefix = name.strip_prefix("drop_").unwrap();
190 let without_suffix = without_prefix.strip_suffix("_table").unwrap();
191 return without_suffix.to_string();
192 }
193
194 name.to_string()
196}
197
198fn migration_template(table_name: &str, table_enum_name: &str) -> String {
199 format!(
200 r#"use sea_orm_migration::prelude::*;
201
202#[derive(DeriveMigrationName)]
203pub struct Migration;
204
205#[async_trait::async_trait]
206impl MigrationTrait for Migration {{
207 async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {{
208 manager
209 .create_table(
210 Table::create()
211 .table({table_enum_name}::Table)
212 .if_not_exists()
213 .col(
214 ColumnDef::new({table_enum_name}::Id)
215 .integer()
216 .not_null()
217 .auto_increment()
218 .primary_key(),
219 )
220 .col(
221 ColumnDef::new({table_enum_name}::CreatedAt)
222 .timestamp_with_time_zone()
223 .not_null()
224 .default(Expr::current_timestamp()),
225 )
226 .col(
227 ColumnDef::new({table_enum_name}::UpdatedAt)
228 .timestamp_with_time_zone()
229 .not_null()
230 .default(Expr::current_timestamp()),
231 )
232 .to_owned(),
233 )
234 .await
235 }}
236
237 async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {{
238 manager
239 .drop_table(Table::drop().table({table_enum_name}::Table).to_owned())
240 .await
241 }}
242}}
243
244/// Table and column identifiers for {table_name}
245#[derive(DeriveIden)]
246enum {table_enum_name} {{
247 Table,
248 Id,
249 CreatedAt,
250 UpdatedAt,
251}}
252"#
253 )
254}
255
256fn migrator_mod_template(migration_name: &str) -> String {
257 format!(
258 r#"pub use sea_orm_migration::prelude::*;
259
260mod {migration_name};
261
262pub struct Migrator;
263
264#[async_trait::async_trait]
265impl MigratorTrait for Migrator {{
266 fn migrations() -> Vec<Box<dyn MigrationTrait>> {{
267 vec![
268 Box::new({migration_name}::Migration),
269 ]
270 }}
271}}
272"#
273 )
274}
275
276fn update_mod_file(mod_file: &Path, migration_name: &str) -> Result<(), String> {
277 let content =
278 fs::read_to_string(mod_file).map_err(|e| format!("Failed to read mod.rs: {e}"))?;
279
280 let mod_decl = format!("mod {migration_name};");
281
282 if content.contains(&mod_decl) {
284 return Ok(());
285 }
286
287 let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
288
289 let mut last_mod_idx = None;
291 for (i, line) in lines.iter().enumerate() {
292 if line.trim().starts_with("mod ") && !line.contains("mod tests") {
293 last_mod_idx = Some(i);
294 }
295 }
296
297 let insert_idx = match last_mod_idx {
299 Some(idx) => idx + 1,
300 None => {
301 let mut insert_idx = 0;
303 for (i, line) in lines.iter().enumerate() {
304 if line.contains("sea_orm_migration") || line.is_empty() {
305 insert_idx = i + 1;
306 } else if line.starts_with("mod ") || line.starts_with("pub struct") {
307 break;
308 }
309 }
310 insert_idx
311 }
312 };
313 lines.insert(insert_idx, mod_decl);
314
315 let box_new_line = format!(" Box::new({migration_name}::Migration),");
317 let mut insert_vec_idx = None;
318
319 for (i, line) in lines.iter().enumerate() {
320 if line.contains("vec![]") {
322 lines[i] = line.replace("vec![]", &format!("vec![\n{box_new_line}\n ]"));
324 let new_content = lines.join("\n");
325 fs::write(mod_file, new_content).map_err(|e| format!("Failed to write mod.rs: {e}"))?;
326 return Ok(());
327 }
328 if line.contains("vec![") && !line.contains("vec![]") {
330 for (j, inner_line) in lines.iter().enumerate().skip(i + 1) {
332 if inner_line.trim() == "]" || inner_line.trim().starts_with("]") {
333 insert_vec_idx = Some(j);
334 break;
335 }
336 }
337 break;
338 }
339 }
340
341 if let Some(idx) = insert_vec_idx {
342 lines.insert(idx, box_new_line);
343 }
344
345 let new_content = lines.join("\n");
346 fs::write(mod_file, new_content).map_err(|e| format!("Failed to write mod.rs: {e}"))?;
347
348 Ok(())
349}