use super::{ContentType, ContentTypeRegistry};
use std::collections::HashMap;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MigrationError {
SourceNotFound {
app_label: String,
model: String,
},
TargetExists {
app_label: String,
model: String,
},
InvalidParameters(String),
}
impl std::fmt::Display for MigrationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SourceNotFound { app_label, model } => {
write!(f, "Source content type not found: {}.{}", app_label, model)
}
Self::TargetExists { app_label, model } => {
write!(
f,
"Target content type already exists: {}.{}",
app_label, model
)
}
Self::InvalidParameters(msg) => write!(f, "Invalid migration parameters: {}", msg),
}
}
}
impl std::error::Error for MigrationError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MigrationRecord {
pub old_app_label: String,
pub old_model: String,
pub new_app_label: String,
pub new_model: String,
pub timestamp: u64,
}
impl MigrationRecord {
#[must_use]
pub fn new(
old_app_label: impl Into<String>,
old_model: impl Into<String>,
new_app_label: impl Into<String>,
new_model: impl Into<String>,
) -> Self {
Self {
old_app_label: old_app_label.into(),
old_model: old_model.into(),
new_app_label: new_app_label.into(),
new_model: new_model.into(),
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
}
}
#[must_use]
pub fn old_qualified_name(&self) -> String {
format!("{}.{}", self.old_app_label, self.old_model)
}
#[must_use]
pub fn new_qualified_name(&self) -> String {
format!("{}.{}", self.new_app_label, self.new_model)
}
#[must_use]
pub fn is_app_rename(&self) -> bool {
self.old_app_label != self.new_app_label
}
#[must_use]
pub fn is_model_rename(&self) -> bool {
self.old_model != self.new_model
}
}
#[derive(Debug, Clone)]
pub struct MigrationResult {
pub records: Vec<MigrationRecord>,
pub migrated_count: usize,
pub warnings: Vec<String>,
}
impl MigrationResult {
#[must_use]
pub fn new() -> Self {
Self {
records: Vec::new(),
migrated_count: 0,
warnings: Vec::new(),
}
}
#[must_use]
pub fn has_migrations(&self) -> bool {
self.migrated_count > 0
}
#[must_use]
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
}
impl Default for MigrationResult {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Default)]
pub struct ContentTypeMigration {
history: Vec<MigrationRecord>,
rename_map: HashMap<String, String>,
}
impl ContentTypeMigration {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn history(&self) -> &[MigrationRecord] {
&self.history
}
pub fn clear_history(&mut self) {
self.history.clear();
self.rename_map.clear();
}
pub fn rename_model(
&mut self,
registry: &ContentTypeRegistry,
app_label: &str,
old_model: &str,
new_model: &str,
) -> Result<MigrationRecord, MigrationError> {
self.rename(registry, app_label, old_model, app_label, new_model)
}
pub fn rename_app(
&mut self,
registry: &ContentTypeRegistry,
old_app_label: &str,
new_app_label: &str,
) -> Result<MigrationResult, MigrationError> {
if old_app_label == new_app_label {
return Err(MigrationError::InvalidParameters(
"Old and new app labels are the same".to_string(),
));
}
let types_to_migrate: Vec<ContentType> = registry
.all()
.into_iter()
.filter(|ct| ct.app_label == old_app_label)
.collect();
if types_to_migrate.is_empty() {
return Err(MigrationError::SourceNotFound {
app_label: old_app_label.to_string(),
model: "*".to_string(),
});
}
for ct in &types_to_migrate {
if registry.get(new_app_label, &ct.model).is_some() {
return Err(MigrationError::TargetExists {
app_label: new_app_label.to_string(),
model: ct.model.clone(),
});
}
}
let mut result = MigrationResult::new();
for ct in types_to_migrate {
let record =
self.rename(registry, &ct.app_label, &ct.model, new_app_label, &ct.model)?;
result.records.push(record);
result.migrated_count += 1;
}
Ok(result)
}
pub fn move_model(
&mut self,
registry: &ContentTypeRegistry,
old_app_label: &str,
model: &str,
new_app_label: &str,
) -> Result<MigrationRecord, MigrationError> {
self.rename(registry, old_app_label, model, new_app_label, model)
}
pub fn rename(
&mut self,
registry: &ContentTypeRegistry,
old_app_label: &str,
old_model: &str,
new_app_label: &str,
new_model: &str,
) -> Result<MigrationRecord, MigrationError> {
if old_app_label == new_app_label && old_model == new_model {
return Err(MigrationError::InvalidParameters(
"Source and target are the same".to_string(),
));
}
if registry.get(old_app_label, old_model).is_none() {
return Err(MigrationError::SourceNotFound {
app_label: old_app_label.to_string(),
model: old_model.to_string(),
});
}
if registry.get(new_app_label, new_model).is_some() {
return Err(MigrationError::TargetExists {
app_label: new_app_label.to_string(),
model: new_model.to_string(),
});
}
let remaining: Vec<ContentType> = registry
.all()
.into_iter()
.filter(|ct| !(ct.app_label == old_app_label && ct.model == old_model))
.collect();
registry.clear();
for ct in remaining {
registry.register(ct);
}
registry.register(ContentType::new(new_app_label, new_model));
let record = MigrationRecord::new(old_app_label, old_model, new_app_label, new_model);
let old_key = record.old_qualified_name();
let new_key = record.new_qualified_name();
self.rename_map.insert(old_key, new_key);
self.history.push(record.clone());
Ok(record)
}
#[must_use]
pub fn resolve_old_name(&self, old_qualified_name: &str) -> Option<String> {
let mut current = old_qualified_name.to_string();
let mut seen = std::collections::HashSet::new();
while let Some(new_name) = self.rename_map.get(¤t) {
if !seen.insert(current.clone()) {
break;
}
current = new_name.clone();
}
if current == old_qualified_name && !self.rename_map.contains_key(¤t) {
None
} else {
Some(current)
}
}
#[must_use]
pub fn export_history(&self) -> Vec<MigrationRecord> {
self.history.clone()
}
pub fn import_history(&mut self, records: Vec<MigrationRecord>) {
for record in records {
let old_key = record.old_qualified_name();
let new_key = record.new_qualified_name();
self.rename_map.insert(old_key, new_key);
self.history.push(record);
}
}
}
pub fn create_model_rename(
registry: &ContentTypeRegistry,
app_label: &str,
old_model: &str,
new_model: &str,
) -> Result<MigrationRecord, MigrationError> {
let mut migration = ContentTypeMigration::new();
migration.rename_model(registry, app_label, old_model, new_model)
}
pub fn create_app_rename(
registry: &ContentTypeRegistry,
old_app_label: &str,
new_app_label: &str,
) -> Result<MigrationResult, MigrationError> {
let mut migration = ContentTypeMigration::new();
migration.rename_app(registry, old_app_label, new_app_label)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_migration_record_creation() {
let record = MigrationRecord::new("old_app", "old_model", "new_app", "new_model");
assert_eq!(record.old_app_label, "old_app");
assert_eq!(record.old_model, "old_model");
assert_eq!(record.new_app_label, "new_app");
assert_eq!(record.new_model, "new_model");
assert!(record.timestamp > 0);
}
#[test]
fn test_migration_record_qualified_names() {
let record = MigrationRecord::new("blog", "article", "posts", "post");
assert_eq!(record.old_qualified_name(), "blog.article");
assert_eq!(record.new_qualified_name(), "posts.post");
}
#[test]
fn test_migration_record_rename_checks() {
let app_rename = MigrationRecord::new("old_app", "model", "new_app", "model");
assert!(app_rename.is_app_rename());
assert!(!app_rename.is_model_rename());
let model_rename = MigrationRecord::new("app", "old_model", "app", "new_model");
assert!(!model_rename.is_app_rename());
assert!(model_rename.is_model_rename());
let both_rename = MigrationRecord::new("old_app", "old_model", "new_app", "new_model");
assert!(both_rename.is_app_rename());
assert!(both_rename.is_model_rename());
}
#[test]
fn test_rename_model() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("blog", "old_article"));
let mut migration = ContentTypeMigration::new();
let record = migration
.rename_model(®istry, "blog", "old_article", "new_article")
.unwrap();
assert_eq!(record.old_model, "old_article");
assert_eq!(record.new_model, "new_article");
assert!(registry.get("blog", "new_article").is_some());
assert!(registry.get("blog", "old_article").is_none());
}
#[test]
fn test_rename_app() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("old_app", "model1"));
registry.register(ContentType::new("old_app", "model2"));
let mut migration = ContentTypeMigration::new();
let result = migration
.rename_app(®istry, "old_app", "new_app")
.unwrap();
assert_eq!(result.migrated_count, 2);
assert!(registry.get("new_app", "model1").is_some());
assert!(registry.get("new_app", "model2").is_some());
assert!(registry.get("old_app", "model1").is_none());
assert!(registry.get("old_app", "model2").is_none());
}
#[test]
fn test_move_model() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("app1", "model"));
let mut migration = ContentTypeMigration::new();
let record = migration
.move_model(®istry, "app1", "model", "app2")
.unwrap();
assert_eq!(record.old_app_label, "app1");
assert_eq!(record.new_app_label, "app2");
assert!(registry.get("app2", "model").is_some());
assert!(registry.get("app1", "model").is_none());
}
#[test]
fn test_rename_source_not_found() {
let registry = ContentTypeRegistry::new();
let mut migration = ContentTypeMigration::new();
let result = migration.rename_model(®istry, "blog", "nonexistent", "new_name");
assert!(matches!(result, Err(MigrationError::SourceNotFound { .. })));
}
#[test]
fn test_rename_target_exists() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("blog", "article"));
registry.register(ContentType::new("blog", "existing"));
let mut migration = ContentTypeMigration::new();
let result = migration.rename_model(®istry, "blog", "article", "existing");
assert!(matches!(result, Err(MigrationError::TargetExists { .. })));
}
#[test]
fn test_rename_same_source_and_target() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("blog", "article"));
let mut migration = ContentTypeMigration::new();
let result = migration.rename_model(®istry, "blog", "article", "article");
assert!(matches!(result, Err(MigrationError::InvalidParameters(_))));
}
#[test]
fn test_migration_history() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("app", "model1"));
registry.register(ContentType::new("app", "model2"));
let mut migration = ContentTypeMigration::new();
migration
.rename_model(®istry, "app", "model1", "new_model1")
.unwrap();
migration
.rename_model(®istry, "app", "model2", "new_model2")
.unwrap();
let history = migration.history();
assert_eq!(history.len(), 2);
}
#[test]
fn test_clear_history() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("app", "model"));
let mut migration = ContentTypeMigration::new();
migration
.rename_model(®istry, "app", "model", "new_model")
.unwrap();
assert_eq!(migration.history().len(), 1);
migration.clear_history();
assert_eq!(migration.history().len(), 0);
}
#[test]
fn test_resolve_old_name() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("app", "model"));
let mut migration = ContentTypeMigration::new();
migration
.rename_model(®istry, "app", "model", "renamed_model")
.unwrap();
let resolved = migration.resolve_old_name("app.model");
assert_eq!(resolved, Some("app.renamed_model".to_string()));
let not_found = migration.resolve_old_name("nonexistent.model");
assert!(not_found.is_none());
}
#[test]
fn test_resolve_chained_renames() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("app", "model"));
let mut migration = ContentTypeMigration::new();
migration
.rename_model(®istry, "app", "model", "model2")
.unwrap();
migration
.rename_model(®istry, "app", "model2", "model3")
.unwrap();
let resolved = migration.resolve_old_name("app.model");
assert_eq!(resolved, Some("app.model3".to_string()));
}
#[test]
fn test_export_import_history() {
let registry1 = ContentTypeRegistry::new();
registry1.register(ContentType::new("app", "model"));
let mut migration1 = ContentTypeMigration::new();
migration1
.rename_model(®istry1, "app", "model", "new_model")
.unwrap();
let exported = migration1.export_history();
let mut migration2 = ContentTypeMigration::new();
migration2.import_history(exported);
assert_eq!(migration2.history().len(), 1);
assert_eq!(
migration2.resolve_old_name("app.model"),
Some("app.new_model".to_string())
);
}
#[test]
fn test_migration_result() {
let mut result = MigrationResult::new();
assert!(!result.has_migrations());
assert!(!result.has_warnings());
result.migrated_count = 2;
result.warnings.push("Warning 1".to_string());
assert!(result.has_migrations());
assert!(result.has_warnings());
}
#[test]
fn test_helper_create_model_rename() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("blog", "article"));
let record = create_model_rename(®istry, "blog", "article", "post").unwrap();
assert_eq!(record.old_model, "article");
assert_eq!(record.new_model, "post");
}
#[test]
fn test_helper_create_app_rename() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("old_blog", "article"));
let result = create_app_rename(®istry, "old_blog", "blog").unwrap();
assert_eq!(result.migrated_count, 1);
assert!(registry.get("blog", "article").is_some());
}
#[test]
fn test_migration_error_display() {
let source_not_found = MigrationError::SourceNotFound {
app_label: "app".to_string(),
model: "model".to_string(),
};
assert!(source_not_found.to_string().contains("not found"));
let target_exists = MigrationError::TargetExists {
app_label: "app".to_string(),
model: "model".to_string(),
};
assert!(target_exists.to_string().contains("already exists"));
let invalid = MigrationError::InvalidParameters("test".to_string());
assert!(invalid.to_string().contains("Invalid"));
}
}