use super::{ContentType, ContentTypeRegistry};
use std::collections::HashSet;
#[derive(Debug, Clone, Default)]
pub struct CleanupResult {
pub removed: Vec<ContentType>,
pub kept: Vec<ContentType>,
pub errors: Vec<String>,
}
impl CleanupResult {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.removed.is_empty()
}
#[must_use]
pub fn total_processed(&self) -> usize {
self.removed.len() + self.kept.len()
}
#[must_use]
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct ContentTypeCleanupManager {
active_types: HashSet<String>,
_on_remove_callbacks: Vec<String>,
}
impl ContentTypeCleanupManager {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn mark_as_active(&mut self, app_label: &str, model: &str) {
self.active_types.insert(format!("{}.{}", app_label, model));
}
pub fn mark_content_type_active(&mut self, content_type: &ContentType) {
self.mark_as_active(&content_type.app_label, &content_type.model);
}
pub fn mark_all_active(&mut self, content_types: &[ContentType]) {
for ct in content_types {
self.mark_content_type_active(ct);
}
}
#[must_use]
pub fn is_active(&self, app_label: &str, model: &str) -> bool {
self.active_types
.contains(&format!("{}.{}", app_label, model))
}
#[must_use]
pub fn is_content_type_active(&self, content_type: &ContentType) -> bool {
self.is_active(&content_type.app_label, &content_type.model)
}
#[must_use]
pub fn active_count(&self) -> usize {
self.active_types.len()
}
pub fn clear_active(&mut self) {
self.active_types.clear();
}
#[must_use]
pub fn find_stale_content_types(&self, registry: &ContentTypeRegistry) -> Vec<ContentType> {
registry
.all()
.into_iter()
.filter(|ct| !self.is_content_type_active(ct))
.collect()
}
#[must_use]
pub fn find_stale_for_app(
&self,
registry: &ContentTypeRegistry,
app_label: &str,
) -> Vec<ContentType> {
registry
.all()
.into_iter()
.filter(|ct| ct.app_label == app_label && !self.is_content_type_active(ct))
.collect()
}
#[must_use]
pub fn dry_run(&self, registry: &ContentTypeRegistry) -> CleanupResult {
let mut result = CleanupResult::new();
for ct in registry.all() {
if self.is_content_type_active(&ct) {
result.kept.push(ct);
} else {
result.removed.push(ct);
}
}
result
}
pub fn cleanup(&self, registry: &ContentTypeRegistry) -> CleanupResult {
let mut result = CleanupResult::new();
let all_types: Vec<ContentType> = registry.all();
for ct in all_types {
if self.is_content_type_active(&ct) {
result.kept.push(ct);
} else {
result.removed.push(ct);
}
}
registry.clear();
for ct in &result.kept {
registry.register(ct.clone());
}
result
}
pub fn remove_content_type(
&self,
registry: &ContentTypeRegistry,
app_label: &str,
model: &str,
) -> Option<ContentType> {
let ct = registry.get(app_label, model)?;
let remaining: Vec<ContentType> = registry
.all()
.into_iter()
.filter(|c| !(c.app_label == app_label && c.model == model))
.collect();
registry.clear();
for c in remaining {
registry.register(c);
}
Some(ct)
}
}
pub fn on_model_unregistered(
registry: &ContentTypeRegistry,
app_label: &str,
model: &str,
) -> Option<ContentType> {
let manager = ContentTypeCleanupManager::new();
manager.remove_content_type(registry, app_label, model)
}
pub fn on_app_unregistered(registry: &ContentTypeRegistry, app_label: &str) -> Vec<ContentType> {
let types_to_remove: Vec<ContentType> = registry
.all()
.into_iter()
.filter(|ct| ct.app_label == app_label)
.collect();
let remaining: Vec<ContentType> = registry
.all()
.into_iter()
.filter(|ct| ct.app_label != app_label)
.collect();
registry.clear();
for ct in remaining {
registry.register(ct);
}
types_to_remove
}
#[derive(Debug, Clone, Default)]
pub struct CleanupStats {
pub before_count: usize,
pub after_count: usize,
pub removed_count: usize,
pub affected_apps: Vec<String>,
}
impl CleanupStats {
#[must_use]
pub fn from_result(result: &CleanupResult) -> Self {
let mut affected_apps: HashSet<String> = HashSet::new();
for ct in &result.removed {
affected_apps.insert(ct.app_label.clone());
}
Self {
before_count: result.removed.len() + result.kept.len(),
after_count: result.kept.len(),
removed_count: result.removed.len(),
affected_apps: affected_apps.into_iter().collect(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cleanup_manager_new() {
let manager = ContentTypeCleanupManager::new();
assert_eq!(manager.active_count(), 0);
}
#[test]
fn test_mark_as_active() {
let mut manager = ContentTypeCleanupManager::new();
manager.mark_as_active("blog", "article");
assert!(manager.is_active("blog", "article"));
assert!(!manager.is_active("blog", "comment"));
assert_eq!(manager.active_count(), 1);
}
#[test]
fn test_mark_content_type_active() {
let mut manager = ContentTypeCleanupManager::new();
let ct = ContentType::new("auth", "user");
manager.mark_content_type_active(&ct);
assert!(manager.is_content_type_active(&ct));
}
#[test]
fn test_mark_all_active() {
let mut manager = ContentTypeCleanupManager::new();
let types = vec![
ContentType::new("blog", "article"),
ContentType::new("blog", "comment"),
ContentType::new("auth", "user"),
];
manager.mark_all_active(&types);
assert_eq!(manager.active_count(), 3);
}
#[test]
fn test_clear_active() {
let mut manager = ContentTypeCleanupManager::new();
manager.mark_as_active("blog", "article");
manager.mark_as_active("auth", "user");
assert_eq!(manager.active_count(), 2);
manager.clear_active();
assert_eq!(manager.active_count(), 0);
}
#[test]
fn test_find_stale_content_types() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("blog", "article"));
registry.register(ContentType::new("blog", "comment"));
registry.register(ContentType::new("auth", "user"));
let mut manager = ContentTypeCleanupManager::new();
manager.mark_as_active("blog", "article");
manager.mark_as_active("auth", "user");
let stale = manager.find_stale_content_types(®istry);
assert_eq!(stale.len(), 1);
assert_eq!(stale[0].model, "comment");
}
#[test]
fn test_find_stale_for_app() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("blog", "article"));
registry.register(ContentType::new("blog", "comment"));
registry.register(ContentType::new("blog", "tag"));
registry.register(ContentType::new("auth", "user"));
let mut manager = ContentTypeCleanupManager::new();
manager.mark_as_active("blog", "article");
manager.mark_as_active("auth", "user");
let stale = manager.find_stale_for_app(®istry, "blog");
assert_eq!(stale.len(), 2);
}
#[test]
fn test_dry_run() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("blog", "article"));
registry.register(ContentType::new("blog", "comment"));
let mut manager = ContentTypeCleanupManager::new();
manager.mark_as_active("blog", "article");
let result = manager.dry_run(®istry);
assert_eq!(result.kept.len(), 1);
assert_eq!(result.removed.len(), 1);
assert_eq!(result.kept[0].model, "article");
assert_eq!(result.removed[0].model, "comment");
assert!(registry.get("blog", "comment").is_some());
}
#[test]
fn test_cleanup() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("blog", "article"));
registry.register(ContentType::new("blog", "comment"));
registry.register(ContentType::new("auth", "user"));
let mut manager = ContentTypeCleanupManager::new();
manager.mark_as_active("blog", "article");
manager.mark_as_active("auth", "user");
let result = manager.cleanup(®istry);
assert_eq!(result.kept.len(), 2);
assert_eq!(result.removed.len(), 1);
assert!(registry.get("blog", "article").is_some());
assert!(registry.get("auth", "user").is_some());
assert!(registry.get("blog", "comment").is_none());
}
#[test]
fn test_remove_content_type() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("blog", "article"));
registry.register(ContentType::new("blog", "comment"));
let manager = ContentTypeCleanupManager::new();
let removed = manager.remove_content_type(®istry, "blog", "comment");
assert!(removed.is_some());
assert_eq!(removed.unwrap().model, "comment");
assert!(registry.get("blog", "comment").is_none());
assert!(registry.get("blog", "article").is_some());
}
#[test]
fn test_remove_content_type_not_found() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("blog", "article"));
let manager = ContentTypeCleanupManager::new();
let removed = manager.remove_content_type(®istry, "blog", "nonexistent");
assert!(removed.is_none());
}
#[test]
fn test_on_model_unregistered() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("blog", "article"));
registry.register(ContentType::new("blog", "comment"));
let removed = on_model_unregistered(®istry, "blog", "article");
assert!(removed.is_some());
assert!(registry.get("blog", "article").is_none());
assert!(registry.get("blog", "comment").is_some());
}
#[test]
fn test_on_app_unregistered() {
let registry = ContentTypeRegistry::new();
registry.register(ContentType::new("blog", "article"));
registry.register(ContentType::new("blog", "comment"));
registry.register(ContentType::new("auth", "user"));
let removed = on_app_unregistered(®istry, "blog");
assert_eq!(removed.len(), 2);
assert!(registry.get("blog", "article").is_none());
assert!(registry.get("blog", "comment").is_none());
assert!(registry.get("auth", "user").is_some());
}
#[test]
fn test_cleanup_result() {
let mut result = CleanupResult::new();
assert!(result.is_empty());
assert_eq!(result.total_processed(), 0);
assert!(!result.has_errors());
result.removed.push(ContentType::new("blog", "article"));
result.kept.push(ContentType::new("auth", "user"));
assert!(!result.is_empty());
assert_eq!(result.total_processed(), 2);
}
#[test]
fn test_cleanup_stats() {
let mut result = CleanupResult::new();
result.removed.push(ContentType::new("blog", "article"));
result.removed.push(ContentType::new("blog", "comment"));
result.kept.push(ContentType::new("auth", "user"));
let stats = CleanupStats::from_result(&result);
assert_eq!(stats.before_count, 3);
assert_eq!(stats.after_count, 1);
assert_eq!(stats.removed_count, 2);
assert!(stats.affected_apps.contains(&"blog".to_string()));
}
}