Skip to main content

reinhardt_db/migrations/
dependency.rs

1//! Migration dependency types
2//!
3//! This module provides types for managing migration dependencies, including:
4//! - Required dependencies (standard)
5//! - Swappable dependencies (e.g., AUTH_USER_MODEL pattern)
6//! - Optional dependencies (conditional based on app installation or settings)
7//!
8//! # Example
9//!
10//! ```rust
11//! use reinhardt_db::migrations::dependency::{
12//!     MigrationDependency, SwappableDependency, OptionalDependency, DependencyCondition
13//! };
14//!
15//! // Required dependency
16//! let required = MigrationDependency::Required {
17//!     app_label: "auth".to_string(),
18//!     migration_name: "0001_initial".to_string(),
19//! };
20//!
21//! // Swappable dependency (depends on configured User model)
22//! let swappable = MigrationDependency::Swappable(SwappableDependency {
23//!     setting_key: "AUTH_USER_MODEL".to_string(),
24//!     default_app: "auth".to_string(),
25//!     default_model: "User".to_string(),
26//!     migration_name: "0001_initial".to_string(),
27//! });
28//!
29//! // Optional dependency (only if GIS app is installed)
30//! let optional = MigrationDependency::Optional(OptionalDependency {
31//!     app_label: "gis_extension".to_string(),
32//!     migration_name: "0001_enable_postgis".to_string(),
33//!     condition: DependencyCondition::AppInstalled("gis_extension".to_string()),
34//! });
35//! ```
36
37use serde::{Deserialize, Serialize};
38use std::collections::HashSet;
39
40/// A dependency that resolves to different apps based on settings.
41///
42/// This is used for Django's swappable model pattern (e.g., AUTH_USER_MODEL).
43/// When a migration depends on a model that can be swapped out (like the User model),
44/// this dependency type allows the migration system to resolve to the actual
45/// configured model at runtime.
46///
47/// # Example
48///
49/// ```rust
50/// use reinhardt_db::migrations::dependency::SwappableDependency;
51///
52/// let dep = SwappableDependency {
53///     setting_key: "AUTH_USER_MODEL".to_string(),
54///     default_app: "auth".to_string(),
55///     default_model: "User".to_string(),
56///     migration_name: "0001_initial".to_string(),
57/// };
58///
59/// // In Django, AUTH_USER_MODEL might be set to "myapp.CustomUser"
60/// // The migration system would resolve this to ("myapp", "0001_initial")
61/// ```
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct SwappableDependency {
64	/// Setting key to look up (e.g., "AUTH_USER_MODEL")
65	pub setting_key: String,
66
67	/// Default app label if setting is not configured
68	pub default_app: String,
69
70	/// Default model name if setting is not configured
71	pub default_model: String,
72
73	/// Migration name to depend on (typically "0001_initial" or "__first__")
74	pub migration_name: String,
75}
76
77impl SwappableDependency {
78	/// Create a new swappable dependency
79	///
80	/// # Example
81	///
82	/// ```rust
83	/// use reinhardt_db::migrations::dependency::SwappableDependency;
84	///
85	/// let dep = SwappableDependency::new(
86	///     "AUTH_USER_MODEL",
87	///     "auth",
88	///     "User",
89	///     "0001_initial",
90	/// );
91	/// assert_eq!(dep.setting_key, "AUTH_USER_MODEL");
92	/// ```
93	pub fn new(
94		setting_key: impl Into<String>,
95		default_app: impl Into<String>,
96		default_model: impl Into<String>,
97		migration_name: impl Into<String>,
98	) -> Self {
99		Self {
100			setting_key: setting_key.into(),
101			default_app: default_app.into(),
102			default_model: default_model.into(),
103			migration_name: migration_name.into(),
104		}
105	}
106
107	/// Resolve the swappable dependency to an actual app label.
108	///
109	/// This method looks up the setting value and extracts the app label.
110	/// If the setting is not configured, returns the default app.
111	///
112	/// # Arguments
113	///
114	/// * `setting_value` - The value from settings (e.g., "myapp.CustomUser")
115	///
116	/// # Returns
117	///
118	/// The app label to use for the dependency.
119	///
120	/// # Example
121	///
122	/// ```rust
123	/// use reinhardt_db::migrations::dependency::SwappableDependency;
124	///
125	/// let dep = SwappableDependency::new("AUTH_USER_MODEL", "auth", "User", "0001_initial");
126	///
127	/// // With custom setting
128	/// assert_eq!(dep.resolve_app_label(Some("myapp.CustomUser")), "myapp");
129	///
130	/// // Without setting (uses default)
131	/// assert_eq!(dep.resolve_app_label(None), "auth");
132	/// ```
133	pub fn resolve_app_label(&self, setting_value: Option<&str>) -> String {
134		match setting_value {
135			Some(value) => {
136				// Parse "app_label.ModelName" format
137				if let Some((app, _model)) = value.split_once('.') {
138					app.to_string()
139				} else {
140					// If no dot, assume it's just the app label
141					value.to_string()
142				}
143			}
144			None => self.default_app.clone(),
145		}
146	}
147
148	/// Resolve to a dependency tuple (app_label, migration_name).
149	///
150	/// # Example
151	///
152	/// ```rust
153	/// use reinhardt_db::migrations::dependency::SwappableDependency;
154	///
155	/// let dep = SwappableDependency::new("AUTH_USER_MODEL", "auth", "User", "0001_initial");
156	/// let (app, migration) = dep.resolve(Some("custom_auth.MyUser"));
157	///
158	/// assert_eq!(app, "custom_auth");
159	/// assert_eq!(migration, "0001_initial");
160	/// ```
161	pub fn resolve(&self, setting_value: Option<&str>) -> (String, String) {
162		(
163			self.resolve_app_label(setting_value),
164			self.migration_name.clone(),
165		)
166	}
167}
168
169/// Conditions for optional dependencies.
170///
171/// Optional dependencies are only enforced when their condition is met.
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173pub enum DependencyCondition {
174	/// Dependency is required only if the specified app is installed.
175	///
176	/// # Example
177	///
178	/// ```rust
179	/// use reinhardt_db::migrations::dependency::DependencyCondition;
180	///
181	/// // Depend on GIS extension only if it's installed
182	/// let condition = DependencyCondition::AppInstalled("gis_extension".to_string());
183	/// ```
184	AppInstalled(String),
185
186	/// Dependency is required only if the specified setting is enabled (truthy).
187	///
188	/// # Example
189	///
190	/// ```rust
191	/// use reinhardt_db::migrations::dependency::DependencyCondition;
192	///
193	/// // Depend on audit tables only if ENABLE_AUDIT_LOGGING is true
194	/// let condition = DependencyCondition::SettingEnabled("ENABLE_AUDIT_LOGGING".to_string());
195	/// ```
196	SettingEnabled(String),
197
198	/// Dependency is required only if the specified feature flag is enabled.
199	///
200	/// # Example
201	///
202	/// ```rust
203	/// use reinhardt_db::migrations::dependency::DependencyCondition;
204	///
205	/// // Depend on feature-specific migrations
206	/// let condition = DependencyCondition::FeatureEnabled("advanced_search".to_string());
207	/// ```
208	FeatureEnabled(String),
209}
210
211impl DependencyCondition {
212	/// Check if the condition is satisfied.
213	///
214	/// # Arguments
215	///
216	/// * `installed_apps` - Set of installed app labels
217	/// * `settings` - Function to look up setting values
218	/// * `features` - Set of enabled feature flags
219	///
220	/// # Example
221	///
222	/// ```rust
223	/// use reinhardt_db::migrations::dependency::DependencyCondition;
224	/// use std::collections::HashSet;
225	///
226	/// let mut apps = HashSet::new();
227	/// apps.insert("gis_extension".to_string());
228	///
229	/// let condition = DependencyCondition::AppInstalled("gis_extension".to_string());
230	///
231	/// assert!(condition.is_satisfied(
232	///     &apps,
233	///     &|_| None,
234	///     &HashSet::new(),
235	/// ));
236	/// ```
237	pub fn is_satisfied<F>(
238		&self,
239		installed_apps: &HashSet<String>,
240		settings_lookup: &F,
241		features: &HashSet<String>,
242	) -> bool
243	where
244		F: Fn(&str) -> Option<String>,
245	{
246		match self {
247			DependencyCondition::AppInstalled(app) => installed_apps.contains(app),
248			DependencyCondition::SettingEnabled(key) => {
249				if let Some(value) = settings_lookup(key) {
250					is_truthy(&value)
251				} else {
252					false
253				}
254			}
255			DependencyCondition::FeatureEnabled(feature) => features.contains(feature),
256		}
257	}
258}
259
260/// An optional dependency that is only enforced when a condition is met.
261///
262/// This is useful for migrations that depend on optional features or apps.
263/// For example, a migration might depend on PostGIS extensions only if the
264/// GIS app is installed.
265///
266/// # Example
267///
268/// ```rust
269/// use reinhardt_db::migrations::dependency::{OptionalDependency, DependencyCondition};
270///
271/// let dep = OptionalDependency {
272///     app_label: "gis_extension".to_string(),
273///     migration_name: "0001_enable_postgis".to_string(),
274///     condition: DependencyCondition::AppInstalled("gis_extension".to_string()),
275/// };
276/// ```
277#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
278pub struct OptionalDependency {
279	/// Target app label
280	pub app_label: String,
281
282	/// Target migration name
283	pub migration_name: String,
284
285	/// Condition that must be met for this dependency to be enforced
286	pub condition: DependencyCondition,
287}
288
289impl OptionalDependency {
290	/// Create a new optional dependency
291	///
292	/// # Example
293	///
294	/// ```rust
295	/// use reinhardt_db::migrations::dependency::{OptionalDependency, DependencyCondition};
296	///
297	/// let dep = OptionalDependency::new(
298	///     "gis_extension",
299	///     "0001_enable_postgis",
300	///     DependencyCondition::AppInstalled("gis_extension".to_string()),
301	/// );
302	/// ```
303	pub fn new(
304		app_label: impl Into<String>,
305		migration_name: impl Into<String>,
306		condition: DependencyCondition,
307	) -> Self {
308		Self {
309			app_label: app_label.into(),
310			migration_name: migration_name.into(),
311			condition,
312		}
313	}
314
315	/// Check if this optional dependency should be enforced.
316	///
317	/// # Arguments
318	///
319	/// * `installed_apps` - Set of installed app labels
320	/// * `settings_lookup` - Function to look up setting values
321	/// * `features` - Set of enabled feature flags
322	///
323	/// # Example
324	///
325	/// ```rust
326	/// use reinhardt_db::migrations::dependency::{OptionalDependency, DependencyCondition};
327	/// use std::collections::HashSet;
328	///
329	/// let dep = OptionalDependency::new(
330	///     "gis",
331	///     "0001_initial",
332	///     DependencyCondition::AppInstalled("gis".to_string()),
333	/// );
334	///
335	/// let mut apps = HashSet::new();
336	/// // GIS not installed
337	/// assert!(!dep.should_enforce(&apps, &|_| None, &HashSet::new()));
338	///
339	/// // GIS installed
340	/// apps.insert("gis".to_string());
341	/// assert!(dep.should_enforce(&apps, &|_| None, &HashSet::new()));
342	/// ```
343	pub fn should_enforce<F>(
344		&self,
345		installed_apps: &HashSet<String>,
346		settings_lookup: &F,
347		features: &HashSet<String>,
348	) -> bool
349	where
350		F: Fn(&str) -> Option<String>,
351	{
352		self.condition
353			.is_satisfied(installed_apps, settings_lookup, features)
354	}
355
356	/// Convert to a dependency tuple if the condition is satisfied.
357	///
358	/// Returns `Some((app_label, migration_name))` if the condition is met,
359	/// `None` otherwise.
360	pub fn to_dependency_if_satisfied<F>(
361		&self,
362		installed_apps: &HashSet<String>,
363		settings_lookup: &F,
364		features: &HashSet<String>,
365	) -> Option<(String, String)>
366	where
367		F: Fn(&str) -> Option<String>,
368	{
369		if self.should_enforce(installed_apps, settings_lookup, features) {
370			Some((self.app_label.clone(), self.migration_name.clone()))
371		} else {
372			None
373		}
374	}
375}
376
377/// Unified migration dependency type.
378///
379/// This enum represents all types of dependencies a migration can have:
380/// - Required: Always enforced
381/// - Swappable: Resolves to different apps based on settings
382/// - Optional: Only enforced when a condition is met
383///
384/// # Example
385///
386/// ```rust
387/// use reinhardt_db::migrations::dependency::{
388///     MigrationDependency, SwappableDependency, OptionalDependency, DependencyCondition
389/// };
390///
391/// let deps = vec![
392///     MigrationDependency::Required {
393///         app_label: "auth".to_string(),
394///         migration_name: "0001_initial".to_string(),
395///     },
396///     MigrationDependency::Swappable(SwappableDependency::new(
397///         "AUTH_USER_MODEL",
398///         "auth",
399///         "User",
400///         "0001_initial",
401///     )),
402///     MigrationDependency::Optional(OptionalDependency::new(
403///         "gis",
404///         "0001_initial",
405///         DependencyCondition::AppInstalled("gis".to_string()),
406///     )),
407/// ];
408/// ```
409#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
410pub enum MigrationDependency {
411	/// A standard required dependency.
412	Required {
413		/// Target app label
414		app_label: String,
415		/// Target migration name
416		migration_name: String,
417	},
418
419	/// A swappable dependency that resolves based on settings.
420	Swappable(SwappableDependency),
421
422	/// An optional dependency that is only enforced when a condition is met.
423	Optional(OptionalDependency),
424}
425
426impl MigrationDependency {
427	/// Create a required dependency.
428	///
429	/// # Example
430	///
431	/// ```rust
432	/// use reinhardt_db::migrations::dependency::MigrationDependency;
433	///
434	/// let dep = MigrationDependency::required("auth", "0001_initial");
435	/// ```
436	pub fn required(app_label: impl Into<String>, migration_name: impl Into<String>) -> Self {
437		Self::Required {
438			app_label: app_label.into(),
439			migration_name: migration_name.into(),
440		}
441	}
442
443	/// Create a swappable dependency.
444	///
445	/// # Example
446	///
447	/// ```rust
448	/// use reinhardt_db::migrations::dependency::MigrationDependency;
449	///
450	/// let dep = MigrationDependency::swappable(
451	///     "AUTH_USER_MODEL",
452	///     "auth",
453	///     "User",
454	///     "0001_initial",
455	/// );
456	/// ```
457	pub fn swappable(
458		setting_key: impl Into<String>,
459		default_app: impl Into<String>,
460		default_model: impl Into<String>,
461		migration_name: impl Into<String>,
462	) -> Self {
463		Self::Swappable(SwappableDependency::new(
464			setting_key,
465			default_app,
466			default_model,
467			migration_name,
468		))
469	}
470
471	/// Create an optional dependency with AppInstalled condition.
472	///
473	/// # Example
474	///
475	/// ```rust
476	/// use reinhardt_db::migrations::dependency::MigrationDependency;
477	///
478	/// let dep = MigrationDependency::optional_app(
479	///     "gis",
480	///     "0001_initial",
481	///     "gis",
482	/// );
483	/// ```
484	pub fn optional_app(
485		app_label: impl Into<String>,
486		migration_name: impl Into<String>,
487		required_app: impl Into<String>,
488	) -> Self {
489		Self::Optional(OptionalDependency::new(
490			app_label,
491			migration_name,
492			DependencyCondition::AppInstalled(required_app.into()),
493		))
494	}
495
496	/// Create an optional dependency with SettingEnabled condition.
497	///
498	/// # Example
499	///
500	/// ```rust
501	/// use reinhardt_db::migrations::dependency::MigrationDependency;
502	///
503	/// let dep = MigrationDependency::optional_setting(
504	///     "audit",
505	///     "0001_initial",
506	///     "ENABLE_AUDIT",
507	/// );
508	/// ```
509	pub fn optional_setting(
510		app_label: impl Into<String>,
511		migration_name: impl Into<String>,
512		setting_key: impl Into<String>,
513	) -> Self {
514		Self::Optional(OptionalDependency::new(
515			app_label,
516			migration_name,
517			DependencyCondition::SettingEnabled(setting_key.into()),
518		))
519	}
520
521	/// Create an optional dependency with FeatureEnabled condition.
522	///
523	/// # Example
524	///
525	/// ```rust
526	/// use reinhardt_db::migrations::dependency::MigrationDependency;
527	///
528	/// let dep = MigrationDependency::optional_feature(
529	///     "search",
530	///     "0001_initial",
531	///     "advanced_search",
532	/// );
533	/// ```
534	pub fn optional_feature(
535		app_label: impl Into<String>,
536		migration_name: impl Into<String>,
537		feature: impl Into<String>,
538	) -> Self {
539		Self::Optional(OptionalDependency::new(
540			app_label,
541			migration_name,
542			DependencyCondition::FeatureEnabled(feature.into()),
543		))
544	}
545}
546
547/// Context for resolving dependencies.
548///
549/// This struct holds all the information needed to resolve swappable and
550/// optional dependencies to their actual targets.
551#[derive(Debug, Clone, Default)]
552pub struct DependencyResolutionContext {
553	/// Set of installed app labels
554	pub installed_apps: HashSet<String>,
555
556	/// Map of setting key to value for swappable dependencies
557	pub swappable_settings: std::collections::HashMap<String, String>,
558
559	/// Set of enabled feature flags
560	pub features: HashSet<String>,
561}
562
563impl DependencyResolutionContext {
564	/// Create a new empty context.
565	pub fn new() -> Self {
566		Self::default()
567	}
568
569	/// Add an installed app.
570	pub fn with_app(mut self, app: impl Into<String>) -> Self {
571		self.installed_apps.insert(app.into());
572		self
573	}
574
575	/// Add multiple installed apps.
576	pub fn with_apps(mut self, apps: impl IntoIterator<Item = impl Into<String>>) -> Self {
577		for app in apps {
578			self.installed_apps.insert(app.into());
579		}
580		self
581	}
582
583	/// Add a swappable setting.
584	pub fn with_setting(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
585		self.swappable_settings.insert(key.into(), value.into());
586		self
587	}
588
589	/// Add a feature flag.
590	pub fn with_feature(mut self, feature: impl Into<String>) -> Self {
591		self.features.insert(feature.into());
592		self
593	}
594
595	/// Look up a setting value.
596	pub fn get_setting(&self, key: &str) -> Option<&String> {
597		self.swappable_settings.get(key)
598	}
599
600	/// Check if an app is installed.
601	pub fn is_app_installed(&self, app: &str) -> bool {
602		self.installed_apps.contains(app)
603	}
604
605	/// Check if a feature is enabled.
606	pub fn is_feature_enabled(&self, feature: &str) -> bool {
607		self.features.contains(feature)
608	}
609}
610
611/// Resolver for migration dependencies.
612///
613/// This struct handles the resolution of all dependency types to their
614/// actual (app_label, migration_name) tuples.
615pub struct DependencyResolver<'a> {
616	context: &'a DependencyResolutionContext,
617}
618
619impl<'a> DependencyResolver<'a> {
620	/// Create a new resolver with the given context.
621	pub fn new(context: &'a DependencyResolutionContext) -> Self {
622		Self { context }
623	}
624
625	/// Resolve a single dependency to its actual target.
626	///
627	/// Returns `Some((app_label, migration_name))` if the dependency should be
628	/// enforced, `None` if it's an optional dependency whose condition is not met.
629	///
630	/// # Example
631	///
632	/// ```rust
633	/// use reinhardt_db::migrations::dependency::{
634	///     MigrationDependency, DependencyResolver, DependencyResolutionContext
635	/// };
636	///
637	/// let context = DependencyResolutionContext::new()
638	///     .with_setting("AUTH_USER_MODEL", "custom_auth.CustomUser");
639	///
640	/// let resolver = DependencyResolver::new(&context);
641	///
642	/// let dep = MigrationDependency::swappable(
643	///     "AUTH_USER_MODEL",
644	///     "auth",
645	///     "User",
646	///     "0001_initial",
647	/// );
648	///
649	/// let resolved = resolver.resolve(&dep);
650	/// assert_eq!(resolved, Some(("custom_auth".to_string(), "0001_initial".to_string())));
651	/// ```
652	pub fn resolve(&self, dependency: &MigrationDependency) -> Option<(String, String)> {
653		match dependency {
654			MigrationDependency::Required {
655				app_label,
656				migration_name,
657			} => Some((app_label.clone(), migration_name.clone())),
658
659			MigrationDependency::Swappable(swappable) => {
660				let setting_value = self
661					.context
662					.get_setting(&swappable.setting_key)
663					.map(|s| s.as_str());
664				Some(swappable.resolve(setting_value))
665			}
666
667			MigrationDependency::Optional(optional) => {
668				let settings_lookup = |key: &str| self.context.get_setting(key).cloned();
669
670				optional.to_dependency_if_satisfied(
671					&self.context.installed_apps,
672					&settings_lookup,
673					&self.context.features,
674				)
675			}
676		}
677	}
678
679	/// Resolve multiple dependencies, filtering out unsatisfied optional dependencies.
680	///
681	/// # Example
682	///
683	/// ```rust
684	/// use reinhardt_db::migrations::dependency::{
685	///     MigrationDependency, DependencyResolver, DependencyResolutionContext
686	/// };
687	///
688	/// let context = DependencyResolutionContext::new()
689	///     .with_app("auth");
690	///
691	/// let resolver = DependencyResolver::new(&context);
692	///
693	/// let deps = vec![
694	///     MigrationDependency::required("auth", "0001_initial"),
695	///     MigrationDependency::optional_app("gis", "0001_initial", "gis"),
696	/// ];
697	///
698	/// let resolved = resolver.resolve_all(&deps);
699	/// // Only the required dependency is resolved (gis not installed)
700	/// assert_eq!(resolved.len(), 1);
701	/// assert_eq!(resolved[0], ("auth".to_string(), "0001_initial".to_string()));
702	/// ```
703	pub fn resolve_all(&self, dependencies: &[MigrationDependency]) -> Vec<(String, String)> {
704		dependencies
705			.iter()
706			.filter_map(|dep| self.resolve(dep))
707			.collect()
708	}
709}
710
711/// Check if a string value is "truthy" (non-empty and not "false"/"0"/"no").
712fn is_truthy(value: &str) -> bool {
713	let lower = value.to_lowercase();
714	!value.is_empty() && lower != "false" && lower != "0" && lower != "no" && lower != "off"
715}
716
717#[cfg(test)]
718mod tests {
719	use super::*;
720
721	#[test]
722	fn test_swappable_dependency_resolve_with_setting() {
723		let dep = SwappableDependency::new("AUTH_USER_MODEL", "auth", "User", "0001_initial");
724
725		let (app, migration) = dep.resolve(Some("custom_auth.CustomUser"));
726		assert_eq!(app, "custom_auth");
727		assert_eq!(migration, "0001_initial");
728	}
729
730	#[test]
731	fn test_swappable_dependency_resolve_without_setting() {
732		let dep = SwappableDependency::new("AUTH_USER_MODEL", "auth", "User", "0001_initial");
733
734		let (app, migration) = dep.resolve(None);
735		assert_eq!(app, "auth");
736		assert_eq!(migration, "0001_initial");
737	}
738
739	#[test]
740	fn test_optional_dependency_app_installed() {
741		let dep = OptionalDependency::new(
742			"gis",
743			"0001_initial",
744			DependencyCondition::AppInstalled("gis".to_string()),
745		);
746
747		let mut apps = HashSet::new();
748
749		// Not installed
750		assert!(!dep.should_enforce(&apps, &|_| None, &HashSet::new()));
751
752		// Installed
753		apps.insert("gis".to_string());
754		assert!(dep.should_enforce(&apps, &|_| None, &HashSet::new()));
755	}
756
757	#[test]
758	fn test_optional_dependency_setting_enabled() {
759		let dep = OptionalDependency::new(
760			"audit",
761			"0001_initial",
762			DependencyCondition::SettingEnabled("ENABLE_AUDIT".to_string()),
763		);
764
765		let apps = HashSet::new();
766
767		// Setting not present
768		assert!(!dep.should_enforce(&apps, &|_| None, &HashSet::new()));
769
770		// Setting is false
771		assert!(!dep.should_enforce(
772			&apps,
773			&|key| {
774				if key == "ENABLE_AUDIT" {
775					Some("false".to_string())
776				} else {
777					None
778				}
779			},
780			&HashSet::new()
781		));
782
783		// Setting is true
784		assert!(dep.should_enforce(
785			&apps,
786			&|key| {
787				if key == "ENABLE_AUDIT" {
788					Some("true".to_string())
789				} else {
790					None
791				}
792			},
793			&HashSet::new()
794		));
795	}
796
797	#[test]
798	fn test_dependency_resolver() {
799		let context = DependencyResolutionContext::new()
800			.with_app("auth")
801			.with_app("users")
802			.with_setting("AUTH_USER_MODEL", "custom_auth.CustomUser");
803
804		let resolver = DependencyResolver::new(&context);
805
806		// Required dependency
807		let required = MigrationDependency::required("auth", "0001_initial");
808		assert_eq!(
809			resolver.resolve(&required),
810			Some(("auth".to_string(), "0001_initial".to_string()))
811		);
812
813		// Swappable dependency
814		let swappable =
815			MigrationDependency::swappable("AUTH_USER_MODEL", "auth", "User", "0001_initial");
816		assert_eq!(
817			resolver.resolve(&swappable),
818			Some(("custom_auth".to_string(), "0001_initial".to_string()))
819		);
820
821		// Optional dependency (satisfied)
822		let optional_satisfied = MigrationDependency::optional_app("auth", "0001_initial", "auth");
823		assert_eq!(
824			resolver.resolve(&optional_satisfied),
825			Some(("auth".to_string(), "0001_initial".to_string()))
826		);
827
828		// Optional dependency (not satisfied)
829		let optional_not_satisfied =
830			MigrationDependency::optional_app("gis", "0001_initial", "gis");
831		assert_eq!(resolver.resolve(&optional_not_satisfied), None);
832	}
833
834	#[test]
835	fn test_resolve_all_filters_unsatisfied() {
836		let context = DependencyResolutionContext::new().with_app("auth");
837
838		let resolver = DependencyResolver::new(&context);
839
840		let deps = vec![
841			MigrationDependency::required("auth", "0001_initial"),
842			MigrationDependency::optional_app("gis", "0001_initial", "gis"),
843			MigrationDependency::required("users", "0001_initial"),
844		];
845
846		let resolved = resolver.resolve_all(&deps);
847		assert_eq!(resolved.len(), 2);
848		assert!(resolved.contains(&("auth".to_string(), "0001_initial".to_string())));
849		assert!(resolved.contains(&("users".to_string(), "0001_initial".to_string())));
850	}
851
852	#[test]
853	fn test_is_truthy() {
854		assert!(is_truthy("true"));
855		assert!(is_truthy("True"));
856		assert!(is_truthy("TRUE"));
857		assert!(is_truthy("1"));
858		assert!(is_truthy("yes"));
859		assert!(is_truthy("on"));
860		assert!(is_truthy("enabled"));
861
862		assert!(!is_truthy("false"));
863		assert!(!is_truthy("False"));
864		assert!(!is_truthy("FALSE"));
865		assert!(!is_truthy("0"));
866		assert!(!is_truthy("no"));
867		assert!(!is_truthy("off"));
868		assert!(!is_truthy(""));
869	}
870}