a3s-orm 0.3.0

A type-safe, executor-neutral SQL query builder for Rust
Documentation
use std::collections::HashSet;

use super::{Migration, MigrationBackend, MigrationError, MigrationRunError, PreparedMigration};

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MigrationReport {
    pub applied: Vec<String>,
}

impl MigrationReport {
    pub fn is_up_to_date(&self) -> bool {
        self.applied.is_empty()
    }
}

pub struct Migrator<B> {
    backend: B,
}

impl<B: MigrationBackend> Migrator<B> {
    pub const fn new(backend: B) -> Self {
        Self { backend }
    }

    pub fn backend(&self) -> &B {
        &self.backend
    }

    pub async fn run(
        &self,
        migrations: impl IntoIterator<Item = Migration>,
    ) -> Result<MigrationReport, MigrationRunError<B::Error>> {
        let migrations = prepare(migrations)?;
        self.backend
            .apply(&migrations)
            .await
            .map_err(MigrationRunError::Backend)
    }
}

fn prepare(
    migrations: impl IntoIterator<Item = Migration>,
) -> Result<Vec<PreparedMigration>, MigrationError> {
    let mut migrations = migrations.into_iter().collect::<Vec<_>>();
    for migration in &migrations {
        validate(migration)?;
    }
    migrations.sort_by(|left, right| left.version().cmp(right.version()));
    let mut versions = HashSet::with_capacity(migrations.len());
    for migration in &migrations {
        if !versions.insert(migration.version().to_owned()) {
            return Err(MigrationError::DuplicateVersion(
                migration.version().to_owned(),
            ));
        }
    }
    Ok(migrations
        .into_iter()
        .map(PreparedMigration::prepare)
        .collect())
}

fn validate(migration: &Migration) -> Result<(), MigrationError> {
    if migration.version().is_empty() {
        return Err(MigrationError::EmptyVersion);
    }
    if !migration
        .version()
        .bytes()
        .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
    {
        return Err(MigrationError::InvalidVersion(
            migration.version().to_owned(),
        ));
    }
    if migration.name().trim().is_empty() {
        return Err(MigrationError::EmptyName {
            version: migration.version().to_owned(),
        });
    }
    if migration.up_sql().trim().is_empty() {
        return Err(MigrationError::EmptySql {
            version: migration.version().to_owned(),
        });
    }
    Ok(())
}