use std::collections::HashMap; use std::fs; use std::path::Path; use quote::{quote, ToTokens}; use structopt::StructOpt; use crate::db::migration::{CreateModelOption, DatabaseChange, Migration}; use crate::db::models::{Field, ModelMeta}; use crate::{Settings, UrlMap}; #[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::{DatabaseChange, Migration}; use tosin::db::models::Field; 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_mod_rs = Path::new(app.mod_rs_path); let app_folder = app_mod_rs .parent() .expect("app mod.rs lives at filesystem root???"); let migrations_dir = app_folder.join("migrations"); let file_path = migrations_dir.join(file_name); println!("Saving migration to {}...", file_path.display()); fs::write(file_path, file_text).unwrap(); // update mod.rs bc the fancy gather!() shit doesn't actually invalidate partial compilation let migrations: Vec = migrations_dir .read_dir() .unwrap() .map(Result::unwrap) .map(|x| x.path().file_stem().unwrap().to_string_lossy().into_owned()) .filter(|x| x != "mod") .map(|x| syn::parse_str(&x).unwrap()) .collect(); let mod_file = quote! { use tosin::db::migration::Migration; #( mod #migrations; )* pub const ALL: &[Migration] = &[ #(#migrations::MIGRATION),* ]; }; let mod_file_path = migrations_dir.join("mod.rs"); let mod_file_text = mod_file.into_token_stream().to_string(); fs::write(mod_file_path, mod_file_text).unwrap(); // TODO cargo fmt } } }