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}