reinhardt-db 0.1.2

Django-style database layer for Reinhardt framework
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
//! ContentType cleanup management
//!
//! This module provides utilities for cleaning up stale or orphaned content types,
//! similar to Django's `remove_stale_contenttypes` management command.
//!
//! # Example
//!
//! ```rust
//! use reinhardt_db::contenttypes::cleanup::{ContentTypeCleanupManager, CleanupResult};
//! use reinhardt_db::contenttypes::{ContentType, ContentTypeRegistry};
//!
//! 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");
//!
//! // "blog.comment" is now considered stale since it wasn't marked as active
//! let stale = manager.find_stale_content_types(&registry);
//! assert_eq!(stale.len(), 1);
//! ```

use super::{ContentType, ContentTypeRegistry};
use std::collections::HashSet;

/// Result of a cleanup operation
#[derive(Debug, Clone, Default)]
pub struct CleanupResult {
	/// Content types that were removed
	pub removed: Vec<ContentType>,
	/// Content types that were kept (active)
	pub kept: Vec<ContentType>,
	/// Errors encountered during cleanup
	pub errors: Vec<String>,
}

impl CleanupResult {
	/// Creates a new empty cleanup result
	#[must_use]
	pub fn new() -> Self {
		Self::default()
	}

	/// Returns true if no content types were removed
	#[must_use]
	pub fn is_empty(&self) -> bool {
		self.removed.is_empty()
	}

	/// Returns the total number of content types processed
	#[must_use]
	pub fn total_processed(&self) -> usize {
		self.removed.len() + self.kept.len()
	}

	/// Returns true if there were any errors
	#[must_use]
	pub fn has_errors(&self) -> bool {
		!self.errors.is_empty()
	}
}

/// Manages cleanup of stale content types
///
/// This manager tracks which content types are actively in use and can identify
/// stale content types that should be removed from the registry.
#[derive(Debug, Clone, Default)]
pub struct ContentTypeCleanupManager {
	/// Set of qualified names (app_label.model) that are considered active
	active_types: HashSet<String>,
	/// Callbacks to invoke when a content type is removed (reserved for future extension)
	_on_remove_callbacks: Vec<String>,
}

impl ContentTypeCleanupManager {
	/// Creates a new cleanup manager
	#[must_use]
	pub fn new() -> Self {
		Self::default()
	}

	/// Marks a content type as active (in use)
	pub fn mark_as_active(&mut self, app_label: &str, model: &str) {
		self.active_types.insert(format!("{}.{}", app_label, model));
	}

	/// Marks a content type as active using a ContentType reference
	pub fn mark_content_type_active(&mut self, content_type: &ContentType) {
		self.mark_as_active(&content_type.app_label, &content_type.model);
	}

	/// Marks multiple content types as active
	pub fn mark_all_active(&mut self, content_types: &[ContentType]) {
		for ct in content_types {
			self.mark_content_type_active(ct);
		}
	}

	/// Checks if a content type is marked as active
	#[must_use]
	pub fn is_active(&self, app_label: &str, model: &str) -> bool {
		self.active_types
			.contains(&format!("{}.{}", app_label, model))
	}

	/// Checks if a ContentType is marked as active
	#[must_use]
	pub fn is_content_type_active(&self, content_type: &ContentType) -> bool {
		self.is_active(&content_type.app_label, &content_type.model)
	}

	/// Returns the number of active content types
	#[must_use]
	pub fn active_count(&self) -> usize {
		self.active_types.len()
	}

	/// Clears all active markers
	pub fn clear_active(&mut self) {
		self.active_types.clear();
	}

	/// Finds content types in the registry that are not marked as active
	#[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()
	}

	/// Finds content types for a specific app that are not marked as active
	#[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()
	}

	/// Performs a dry run of cleanup, returning what would be removed
	#[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
	}

	/// Performs cleanup by removing stale content types from the registry
	///
	/// Note: This modifies the registry by clearing it and re-adding only active types.
	/// In a real application, you would typically want to also update the database.
	pub fn cleanup(&self, registry: &ContentTypeRegistry) -> CleanupResult {
		let mut result = CleanupResult::new();

		// Collect all content types
		let all_types: Vec<ContentType> = registry.all();

		// Separate into kept and removed
		for ct in all_types {
			if self.is_content_type_active(&ct) {
				result.kept.push(ct);
			} else {
				result.removed.push(ct);
			}
		}

		// Clear and re-register only active types
		registry.clear();
		for ct in &result.kept {
			registry.register(ct.clone());
		}

		result
	}

	/// Removes a specific content type from the registry
	pub fn remove_content_type(
		&self,
		registry: &ContentTypeRegistry,
		app_label: &str,
		model: &str,
	) -> Option<ContentType> {
		let ct = registry.get(app_label, model)?;

		// Get all types except the one to remove
		let remaining: Vec<ContentType> = registry
			.all()
			.into_iter()
			.filter(|c| !(c.app_label == app_label && c.model == model))
			.collect();

		// Clear and re-register
		registry.clear();
		for c in remaining {
			registry.register(c);
		}

		Some(ct)
	}
}

/// Handles cleanup when a model is unregistered from the system
///
/// This is typically called when an app is uninstalled or a model is removed.
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)
}

/// Handles cleanup when an entire app is unregistered
///
/// This removes all content types for the specified app.
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();

	// Get remaining types
	let remaining: Vec<ContentType> = registry
		.all()
		.into_iter()
		.filter(|ct| ct.app_label != app_label)
		.collect();

	// Clear and re-register remaining
	registry.clear();
	for ct in remaining {
		registry.register(ct);
	}

	types_to_remove
}

/// Statistics about the cleanup operation
#[derive(Debug, Clone, Default)]
pub struct CleanupStats {
	/// Number of content types before cleanup
	pub before_count: usize,
	/// Number of content types after cleanup
	pub after_count: usize,
	/// Number of content types removed
	pub removed_count: usize,
	/// Apps affected by the cleanup
	pub affected_apps: Vec<String>,
}

impl CleanupStats {
	/// Creates new stats from a cleanup result and registry
	#[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(&registry);

		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(&registry, "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(&registry);

		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");

		// Verify registry unchanged after dry run
		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(&registry);

		assert_eq!(result.kept.len(), 2);
		assert_eq!(result.removed.len(), 1);

		// Verify registry was updated
		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(&registry, "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(&registry, "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(&registry, "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(&registry, "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()));
	}
}