use std::collections::HashMap; use std::fs; use std::path::PathBuf; use quote::{quote, ToTokens}; use structopt::StructOpt; use crate::{Settings, UrlMap, db::backend::Connectable}; use crate::db::migration::{Migration, DatabaseChange, CreateModelOption}; use crate::db::models::{Field, ModelMeta}; #[derive(StructOpt)] /// Generate migrations pub struct MakeMigrations { } #[derive(Debug)] struct AppTablesState { db: HashMap<&'static str, TableState>, } #[derive(Debug)] struct TableState { fields: Vec, } impl AppTablesState { fn changes_to(self, dest: AppTablesState) -> Vec { let mut result = vec![]; for table in dest.db.keys() { if !self.db.contains_key(table) { let fields = &dest.db[table].fields.iter().map(|field| { match field { Field::CharField { name, max_length: Some(max_length) } => quote! { Field::CharField { name: #name, max_length: Some(#max_length) } }, Field::CharField { name, max_length: None } => quote! { Field::CharField { name: #name, max_length: None } }, Field::DateTimeField { name } => quote! { Field::DateTimeField { name: #name } }, Field::IntField { name } => quote! { Field::IntField { name: #name } }, } }).collect::>(); result.push(quote! { DatabaseChange::CreateModel { name: #table, fields: &[ #(#fields),* ], options: &[], } }); } } result } } impl From<&[ModelMeta]> for AppTablesState { fn from(models: &[ModelMeta]) -> Self { let mut db = HashMap::new(); for model in models { db.insert(model.name, TableState { fields: model.fields.into() }); } Self { db } } } impl From<&[Migration]> for AppTablesState { fn from(migrations: &[Migration]) -> Self { let mut db = HashMap::new(); for migration in migrations { for change in migration.changes { match change { DatabaseChange::CreateModel { name, fields, options } => { if db.contains_key(name) { if options.contains(&CreateModelOption::IfNotExist) { continue; } else { panic!("double-created table {}", name); } } db.insert(*name, TableState { fields: (*fields).into() }); } } } } Self { db } } } impl MakeMigrations { pub fn execute(self, _urls: UrlMap, settings: Settings) { for app in settings.installed_apps { let expected_table_state = AppTablesState::from(app.models); let actual_table_state = AppTablesState::from(app.migrations); let next_id = app.migrations.iter().map(|m| m.id).max().map_or(1, |x| x + 1); let name = "auto"; // TODO names let changes = actual_table_state.changes_to(expected_table_state); if changes.is_empty() { continue; } let migration = quote! { Migration { id: #next_id, name: #name, prereqs: &[], // TODO changes: &[ #(#changes),* ], } }; let file = quote! { use tosin::db::migration::Migration; pub const MIGRATION: Migration = #migration; }; let file_name = format!("m_{:04}_{}.rs", next_id, name); let file_text = file.into_token_stream().to_string(); let app_folder = app.name.split("::") .skip(1) .collect::(); // TODO don't explode if running in a weird place let file_path = PathBuf::from("src") .join(app_folder) .join("migrations") .join(file_name); println!("Saving migration to {}...", file_path.display()); fs::write(file_path, file_text).unwrap(); // TODO cargo fmt } } }