1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
7use std::error::Error;
8
9macro_rules! migration_text_type {
10 ($type_name:ident) => {
11 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12 pub struct $type_name(String);
13
14 impl $type_name {
15 pub fn new(input: impl AsRef<str>) -> Result<Self, MigrationError> {
21 validate_text(input.as_ref()).map(|value| Self(value.to_owned()))
22 }
23
24 #[must_use]
26 pub fn as_str(&self) -> &str {
27 &self.0
28 }
29 }
30
31 impl fmt::Display for $type_name {
32 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33 formatter.write_str(self.as_str())
34 }
35 }
36 };
37}
38
39migration_text_type!(MigrationId);
40migration_text_type!(MigrationVersion);
41migration_text_type!(MigrationChecksum);
42migration_text_type!(MigrationAppliedAt);
43
44#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
46pub enum MigrationStatus {
47 #[default]
49 Pending,
50 Applied,
52 Failed,
54 Reverted,
56 Unknown,
58}
59
60#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
62pub enum MigrationDirection {
63 #[default]
65 Up,
66 Down,
68}
69
70#[derive(Clone, Debug, Eq, PartialEq)]
72pub struct MigrationStep {
73 id: MigrationId,
74 version: Option<MigrationVersion>,
75 checksum: Option<MigrationChecksum>,
76}
77
78impl MigrationStep {
79 #[must_use]
81 pub const fn new(id: MigrationId) -> Self {
82 Self {
83 id,
84 version: None,
85 checksum: None,
86 }
87 }
88
89 #[must_use]
91 pub fn with_version(mut self, version: MigrationVersion) -> Self {
92 self.version = Some(version);
93 self
94 }
95
96 #[must_use]
98 pub fn with_checksum(mut self, checksum: MigrationChecksum) -> Self {
99 self.checksum = Some(checksum);
100 self
101 }
102
103 #[must_use]
105 pub const fn id(&self) -> &MigrationId {
106 &self.id
107 }
108}
109
110#[derive(Clone, Debug, Eq, PartialEq)]
112pub struct MigrationPlan {
113 direction: MigrationDirection,
114 steps: Vec<MigrationStep>,
115}
116
117impl MigrationPlan {
118 #[must_use]
120 pub const fn new(direction: MigrationDirection, steps: Vec<MigrationStep>) -> Self {
121 Self { direction, steps }
122 }
123
124 #[must_use]
126 pub const fn direction(&self) -> MigrationDirection {
127 self.direction
128 }
129
130 #[must_use]
132 pub fn steps(&self) -> &[MigrationStep] {
133 &self.steps
134 }
135}
136
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
139pub enum MigrationError {
140 Empty,
142 ControlCharacter,
144}
145
146impl fmt::Display for MigrationError {
147 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
148 match self {
149 Self::Empty => formatter.write_str("migration label cannot be empty"),
150 Self::ControlCharacter => {
151 formatter.write_str("migration label cannot contain control characters")
152 },
153 }
154 }
155}
156
157impl Error for MigrationError {}
158
159fn validate_text(input: &str) -> Result<&str, MigrationError> {
160 if input.chars().any(char::is_control) {
161 return Err(MigrationError::ControlCharacter);
162 }
163 let trimmed = input.trim();
164 if trimmed.is_empty() {
165 return Err(MigrationError::Empty);
166 }
167 Ok(trimmed)
168}
169
170#[cfg(test)]
171mod tests {
172 use super::{
173 MigrationDirection, MigrationId, MigrationPlan, MigrationStatus, MigrationStep,
174 MigrationVersion,
175 };
176
177 #[test]
178 fn stores_migration_metadata() -> Result<(), Box<dyn std::error::Error>> {
179 let step = MigrationStep::new(MigrationId::new("create-users")?)
180 .with_version(MigrationVersion::new("202605250001")?);
181 let plan = MigrationPlan::new(MigrationDirection::Up, vec![step]);
182
183 assert_eq!(plan.direction(), MigrationDirection::Up);
184 assert_eq!(plan.steps()[0].id().as_str(), "create-users");
185 assert_eq!(MigrationStatus::default(), MigrationStatus::Pending);
186 Ok(())
187 }
188}