#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::fmt;
use std::error::Error;
macro_rules! migration_text_type {
($type_name:ident) => {
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct $type_name(String);
impl $type_name {
pub fn new(input: impl AsRef<str>) -> Result<Self, MigrationError> {
validate_text(input.as_ref()).map(|value| Self(value.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $type_name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
};
}
migration_text_type!(MigrationId);
migration_text_type!(MigrationVersion);
migration_text_type!(MigrationChecksum);
migration_text_type!(MigrationAppliedAt);
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum MigrationStatus {
#[default]
Pending,
Applied,
Failed,
Reverted,
Unknown,
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum MigrationDirection {
#[default]
Up,
Down,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MigrationStep {
id: MigrationId,
version: Option<MigrationVersion>,
checksum: Option<MigrationChecksum>,
}
impl MigrationStep {
#[must_use]
pub const fn new(id: MigrationId) -> Self {
Self {
id,
version: None,
checksum: None,
}
}
#[must_use]
pub fn with_version(mut self, version: MigrationVersion) -> Self {
self.version = Some(version);
self
}
#[must_use]
pub fn with_checksum(mut self, checksum: MigrationChecksum) -> Self {
self.checksum = Some(checksum);
self
}
#[must_use]
pub const fn id(&self) -> &MigrationId {
&self.id
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MigrationPlan {
direction: MigrationDirection,
steps: Vec<MigrationStep>,
}
impl MigrationPlan {
#[must_use]
pub const fn new(direction: MigrationDirection, steps: Vec<MigrationStep>) -> Self {
Self { direction, steps }
}
#[must_use]
pub const fn direction(&self) -> MigrationDirection {
self.direction
}
#[must_use]
pub fn steps(&self) -> &[MigrationStep] {
&self.steps
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MigrationError {
Empty,
ControlCharacter,
}
impl fmt::Display for MigrationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("migration label cannot be empty"),
Self::ControlCharacter => {
formatter.write_str("migration label cannot contain control characters")
},
}
}
}
impl Error for MigrationError {}
fn validate_text(input: &str) -> Result<&str, MigrationError> {
if input.chars().any(char::is_control) {
return Err(MigrationError::ControlCharacter);
}
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(MigrationError::Empty);
}
Ok(trimmed)
}
#[cfg(test)]
mod tests {
use super::{
MigrationDirection, MigrationId, MigrationPlan, MigrationStatus, MigrationStep,
MigrationVersion,
};
#[test]
fn stores_migration_metadata() -> Result<(), Box<dyn std::error::Error>> {
let step = MigrationStep::new(MigrationId::new("create-users")?)
.with_version(MigrationVersion::new("202605250001")?);
let plan = MigrationPlan::new(MigrationDirection::Up, vec![step]);
assert_eq!(plan.direction(), MigrationDirection::Up);
assert_eq!(plan.steps()[0].id().as_str(), "create-users");
assert_eq!(MigrationStatus::default(), MigrationStatus::Pending);
Ok(())
}
}