barrel 0.7.0

A powerful schema migration building API for Rust
Documentation
//!

// This integration relies on _knowing_ which backend is being used at compile-time
// This is a poor woman's XOR - if you know how to make it more pretty, PRs welcome <3
#[cfg(any(
    all(feature = "pg", feature = "mysql"),
    all(feature = "pg", feature = "sqlite3"),
    all(feature = "mysql", feature = "sqlite3")
))]
compile_error!("`barrel` can only integrate with `diesel` if you select one (1) backend!");

use diesel_rs::connection::SimpleConnection;
use diesel_rs::migration::{Migration, RunMigrationsError};
use std::fs::{self, File};
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::process::Command;

/// Represents a migration run inside Diesel
///
/// 1. Path
/// 2. Version
/// 3. Up
/// 4. Down
pub struct BarrelMigration(PathBuf, String, String, String);

impl Migration for BarrelMigration {
    fn file_path(&self) -> Option<&Path> {
        Some(self.0.as_path())
    }

    fn version(&self) -> &str {
        &self.1
    }

    fn run(&self, conn: &SimpleConnection) -> Result<(), RunMigrationsError> {
        conn.batch_execute(&self.2)?;
        Ok(())
    }

    fn revert(&self, conn: &SimpleConnection) -> Result<(), RunMigrationsError> {
        conn.batch_execute(&self.3)?;
        Ok(())
    }
}

/// Generate migration files using the barrel schema builder
pub fn generate_initial(path: &PathBuf) {
    generate_initial_with_content(
        path,
        &"fn up(migr: &mut Migration) {} \n\n".to_string(),
        &"fn down(migr: &mut Migration) {} \n".to_string(),
    )
}

/// Generate migration files using the barrel schema builder with initial content
pub fn generate_initial_with_content(path: &PathBuf, up_content: &String, down_content: &String) {
    let migr_path = path.join("mod.rs");
    println!("Creating {}", migr_path.display());

    let mut barrel_migr = fs::File::create(migr_path).unwrap();
    barrel_migr.write(b"/// Handle up migrations \n").unwrap();
    barrel_migr.write(up_content.as_bytes()).unwrap();

    barrel_migr.write(b"/// Handle down migrations \n").unwrap();
    barrel_migr.write(down_content.as_bytes()).unwrap();
}

/// Generate a Migration from the provided path
pub fn migration_from(path: &Path) -> Option<Box<Migration>> {
    match path.join("mod.rs").exists() {
        true => Some(run_barrel_migration_wrapper(&path.join("mod.rs"))),
        false => None,
    }
}

fn version_from_path(path: &Path) -> Result<String, ()> {
    path.parent()
        .unwrap_or_else(|| {
            panic!(
                "Migration doesn't appear to be in a directory: `{:?}`",
                path
            )
        })
        .file_name()
        .unwrap_or_else(|| panic!("Can't get file name from path `{:?}`", path))
        .to_string_lossy()
        .split('_')
        .nth(0)
        .map(|s| Ok(s.replace('-', "")))
        .unwrap_or_else(|| Err(()))
}

fn run_barrel_migration_wrapper(path: &Path) -> Box<Migration> {
    let (up, down) = run_barrel_migration(&path);
    let version = version_from_path(path).unwrap();
    let migration_path = match path.parent() {
        Some(parent_path) => parent_path.to_path_buf(),
        None => path.to_path_buf(),
    };
    Box::new(BarrelMigration(migration_path, version, up, down))
}

fn run_barrel_migration(migration: &Path) -> (String, String) {
    /* Create a tmp dir with src/ child */
    use tempfile::Builder;

    let dir = Builder::new().prefix("barrel").tempdir().unwrap();
    fs::create_dir_all(&dir.path().join("src")).unwrap();

    let (feat, ident) = get_backend_pair();

    let toml = format!(
        "# This file is auto generated by barrel
[package]
name = \"tmp-generator\"
description = \"Doing nasty things with cargo\"
version = \"0.0.0\"
authors = [\"Katharina Fey <kookie@spacekookie.de>\"]
# TODO: Use same `barrel` dependency as crate
[dependencies]
barrel = {{ version = \"*\", features = [ {:?} ] }}",
        feat
    );

    /* Add a Cargo.toml file */
    let ct = dir.path().join("Cargo.toml");
    let mut cargo_toml = File::create(&ct).unwrap();
    cargo_toml.write_all(toml.as_bytes()).unwrap();

    /* Generate main.rs based on user migration */
    let main_file_path = &dir.path().join("src").join("main.rs");
    let mut main_file = File::create(&main_file_path).unwrap();

    let user_migration = migration.as_os_str().to_os_string().into_string().unwrap();
    main_file
        .write_all(
            format!(
                "//! This file is auto generated by barrel
extern crate barrel;
use barrel::*;

use barrel::backend::{ident};

include!(\"{}\");

fn main() {{
    let mut m_up = Migration::new();
    up(&mut m_up);
    println!(\"{{}}\", m_up.make::<{ident}>());

    let mut m_down = Migration::new();
    down(&mut m_down);
    println!(\"{{}}\", m_down.make::<{ident}>());
}}
",
                user_migration,
                ident = ident
            )
            .as_bytes(),
        )
        .unwrap();

    let output = if cfg!(target_os = "windows") {
        Command::new("cargo")
            .current_dir(dir.path())
            .arg("run")
            .output()
            .expect("failed to execute cargo!")
    } else {
        Command::new("sh")
            .current_dir(dir.path())
            .arg("-c")
            .arg("cargo run")
            .output()
            .expect("failed to execute cargo!")
    };

    let output = String::from_utf8_lossy(&output.stdout);
    let vec: Vec<&str> = output.split("\n").collect();
    let up = String::from(vec[0]);
    let down = String::from(vec[1]);

    (up, down)
}

/// Uses the fact that barrel with diesel support is only compiled with _one_ feature
///
/// The first string is the feature-name, the other the struct ident
fn get_backend_pair() -> (&'static str, &'static str) {
    #[cfg(feature = "pg")]
    return ("pg", "Pg");
    #[cfg(feature = "mysql")]
    return ("mysql", "Mysql");
    #[cfg(feature = "sqlite3")]
    return ("sqlite3", "Sqlite");
}