Skip to main content

copybook_contracts/
feature_flags.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Feature flag system contract for copybook-rs.
3
4use serde::{Deserialize, Serialize};
5use std::collections::HashSet;
6use std::env;
7use std::fmt;
8use std::str::FromStr;
9use std::sync::{OnceLock, RwLock};
10
11/// All available feature flags.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14#[non_exhaustive]
15pub enum Feature {
16    // ========== Experimental Features ==========
17    /// Enable SIGN SEPARATE clause support (experimental)
18    #[serde(alias = "sign_separate")]
19    SignSeparate,
20
21    /// Enable RENAMES R4-R6 advanced scenarios
22    #[serde(alias = "renames_r4_r6")]
23    RenamesR4R6,
24
25    /// Enable COMP-1 (single precision floating point) support
26    #[serde(alias = "comp_1")]
27    Comp1,
28
29    /// Enable COMP-2 (double precision floating point) support
30    #[serde(alias = "comp_2")]
31    Comp2,
32
33    // ========== Enterprise Features ==========
34    /// Enable audit system for compliance tracking
35    #[serde(alias = "audit_system")]
36    AuditSystem,
37
38    /// Enable SOX compliance validation
39    #[serde(alias = "sox_compliance")]
40    SoxCompliance,
41
42    /// Enable HIPAA compliance validation
43    #[serde(alias = "hipaa_compliance")]
44    HipaaCompliance,
45
46    /// Enable GDPR compliance validation
47    #[serde(alias = "gdpr_compliance")]
48    GdprCompliance,
49
50    /// Enable PCI DSS compliance validation
51    #[serde(alias = "pci_dss_compliance")]
52    PciDssCompliance,
53
54    /// Enable security monitoring integration
55    #[serde(alias = "security_monitoring")]
56    SecurityMonitoring,
57
58    // ========== Performance Features ==========
59    /// Enable advanced optimization mode (SIMD, vectorization)
60    #[serde(alias = "advanced_optimization")]
61    AdvancedOptimization,
62
63    /// Enable LRU cache for parsed copybooks
64    #[serde(alias = "lru_cache")]
65    LruCache,
66
67    /// Enable parallel decoding for large files
68    #[serde(alias = "parallel_decode")]
69    ParallelDecode,
70
71    /// Enable zero-copy parsing where possible
72    #[serde(alias = "zero_copy")]
73    ZeroCopy,
74
75    // ========== Debug Features ==========
76    /// Enable verbose logging with detailed diagnostics
77    #[serde(alias = "verbose_logging")]
78    VerboseLogging,
79
80    /// Enable diagnostic output for troubleshooting
81    #[serde(alias = "diagnostic_output")]
82    DiagnosticOutput,
83
84    /// Enable CPU profiling hooks
85    #[serde(alias = "profiling")]
86    Profiling,
87
88    /// Enable memory usage tracking
89    #[serde(alias = "memory_tracking")]
90    MemoryTracking,
91
92    // ========== Testing Features ==========
93    /// Enable mutation testing hooks
94    #[serde(alias = "mutation_testing")]
95    MutationTesting,
96
97    /// Enable fuzzing integration points
98    #[serde(alias = "fuzzing_integration")]
99    FuzzingIntegration,
100
101    /// Enable test coverage instrumentation
102    #[serde(alias = "coverage_instrumentation")]
103    CoverageInstrumentation,
104
105    /// Enable property-based testing integration
106    #[serde(alias = "property_based_testing")]
107    PropertyBasedTesting,
108}
109
110impl Feature {
111    /// Get the category this feature belongs to.
112    #[inline]
113    #[must_use]
114    pub const fn category(self) -> FeatureCategory {
115        match self {
116            Feature::SignSeparate | Feature::RenamesR4R6 | Feature::Comp1 | Feature::Comp2 => {
117                FeatureCategory::Experimental
118            }
119            Feature::AuditSystem
120            | Feature::SoxCompliance
121            | Feature::HipaaCompliance
122            | Feature::GdprCompliance
123            | Feature::PciDssCompliance
124            | Feature::SecurityMonitoring => FeatureCategory::Enterprise,
125            Feature::AdvancedOptimization
126            | Feature::LruCache
127            | Feature::ParallelDecode
128            | Feature::ZeroCopy => FeatureCategory::Performance,
129            Feature::VerboseLogging
130            | Feature::DiagnosticOutput
131            | Feature::Profiling
132            | Feature::MemoryTracking => FeatureCategory::Debug,
133            Feature::MutationTesting
134            | Feature::FuzzingIntegration
135            | Feature::CoverageInstrumentation
136            | Feature::PropertyBasedTesting => FeatureCategory::Testing,
137        }
138    }
139
140    /// Get the default enabled state for this feature.
141    #[inline]
142    #[must_use]
143    pub const fn default_enabled(self) -> bool {
144        match self {
145            Feature::SignSeparate | Feature::Comp1 | Feature::Comp2 | Feature::LruCache => true,
146            Feature::RenamesR4R6
147            | Feature::AuditSystem
148            | Feature::SoxCompliance
149            | Feature::HipaaCompliance
150            | Feature::GdprCompliance
151            | Feature::PciDssCompliance
152            | Feature::SecurityMonitoring
153            | Feature::AdvancedOptimization
154            | Feature::ParallelDecode
155            | Feature::ZeroCopy
156            | Feature::VerboseLogging
157            | Feature::DiagnosticOutput
158            | Feature::Profiling
159            | Feature::MemoryTracking
160            | Feature::MutationTesting
161            | Feature::FuzzingIntegration
162            | Feature::CoverageInstrumentation
163            | Feature::PropertyBasedTesting => false,
164        }
165    }
166
167    /// Get the environment variable name for this feature.
168    #[inline]
169    #[must_use]
170    pub fn env_var_name(self) -> String {
171        format!("COPYBOOK_FF_{}", self.to_string().to_uppercase())
172    }
173
174    /// Get a human-readable description of this feature.
175    #[inline]
176    #[must_use]
177    pub const fn description(self) -> &'static str {
178        match self {
179            Feature::SignSeparate => "Enable SIGN SEPARATE clause support",
180            Feature::RenamesR4R6 => "Enable RENAMES R4-R6 advanced scenarios",
181            Feature::Comp1 => "Enable COMP-1 (single precision floating point) support",
182            Feature::Comp2 => "Enable COMP-2 (double precision floating point) support",
183            Feature::AuditSystem => "Enable audit system for compliance tracking",
184            Feature::SoxCompliance => "Enable SOX compliance validation",
185            Feature::HipaaCompliance => "Enable HIPAA compliance validation",
186            Feature::GdprCompliance => "Enable GDPR compliance validation",
187            Feature::PciDssCompliance => "Enable PCI DSS compliance validation",
188            Feature::SecurityMonitoring => "Enable security monitoring integration",
189            Feature::AdvancedOptimization => {
190                "Enable advanced optimization mode (SIMD, vectorization)"
191            }
192            Feature::LruCache => "Enable LRU cache for parsed copybooks",
193            Feature::ParallelDecode => "Enable parallel decoding for large files",
194            Feature::ZeroCopy => "Enable zero-copy parsing where possible",
195            Feature::VerboseLogging => "Enable verbose logging with detailed diagnostics",
196            Feature::DiagnosticOutput => "Enable diagnostic output for troubleshooting",
197            Feature::Profiling => "Enable CPU profiling hooks",
198            Feature::MemoryTracking => "Enable memory usage tracking",
199            Feature::MutationTesting => "Enable mutation testing hooks",
200            Feature::FuzzingIntegration => "Enable fuzzing integration points",
201            Feature::CoverageInstrumentation => "Enable test coverage instrumentation",
202            Feature::PropertyBasedTesting => "Enable property-based testing integration",
203        }
204    }
205}
206
207impl fmt::Display for Feature {
208    #[inline]
209    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210        let s = match self {
211            Feature::SignSeparate => "sign_separate",
212            Feature::RenamesR4R6 => "renames_r4_r6",
213            Feature::Comp1 => "comp_1",
214            Feature::Comp2 => "comp_2",
215            Feature::AuditSystem => "audit_system",
216            Feature::SoxCompliance => "sox_compliance",
217            Feature::HipaaCompliance => "hipaa_compliance",
218            Feature::GdprCompliance => "gdpr_compliance",
219            Feature::PciDssCompliance => "pci_dss_compliance",
220            Feature::SecurityMonitoring => "security_monitoring",
221            Feature::AdvancedOptimization => "advanced_optimization",
222            Feature::LruCache => "lru_cache",
223            Feature::ParallelDecode => "parallel_decode",
224            Feature::ZeroCopy => "zero_copy",
225            Feature::VerboseLogging => "verbose_logging",
226            Feature::DiagnosticOutput => "diagnostic_output",
227            Feature::Profiling => "profiling",
228            Feature::MemoryTracking => "memory_tracking",
229            Feature::MutationTesting => "mutation_testing",
230            Feature::FuzzingIntegration => "fuzzing_integration",
231            Feature::CoverageInstrumentation => "coverage_instrumentation",
232            Feature::PropertyBasedTesting => "property_based_testing",
233        };
234        write!(f, "{s}")
235    }
236}
237
238impl FromStr for Feature {
239    type Err = String;
240
241    #[inline]
242    fn from_str(s: &str) -> Result<Self, Self::Err> {
243        match s.to_lowercase().as_str() {
244            "sign_separate" => Ok(Self::SignSeparate),
245            "renames_r4_r6" => Ok(Self::RenamesR4R6),
246            "comp_1" => Ok(Self::Comp1),
247            "comp_2" => Ok(Self::Comp2),
248            "audit_system" => Ok(Self::AuditSystem),
249            "sox_compliance" => Ok(Self::SoxCompliance),
250            "hipaa_compliance" => Ok(Self::HipaaCompliance),
251            "gdpr_compliance" => Ok(Self::GdprCompliance),
252            "pci_dss_compliance" => Ok(Self::PciDssCompliance),
253            "security_monitoring" => Ok(Self::SecurityMonitoring),
254            "advanced_optimization" => Ok(Self::AdvancedOptimization),
255            "lru_cache" => Ok(Self::LruCache),
256            "parallel_decode" => Ok(Self::ParallelDecode),
257            "zero_copy" => Ok(Self::ZeroCopy),
258            "verbose_logging" => Ok(Self::VerboseLogging),
259            "diagnostic_output" => Ok(Self::DiagnosticOutput),
260            "profiling" => Ok(Self::Profiling),
261            "memory_tracking" => Ok(Self::MemoryTracking),
262            "mutation_testing" => Ok(Self::MutationTesting),
263            "fuzzing_integration" => Ok(Self::FuzzingIntegration),
264            "coverage_instrumentation" => Ok(Self::CoverageInstrumentation),
265            "property_based_testing" => Ok(Self::PropertyBasedTesting),
266            _ => Err(format!("Unknown feature flag: '{s}'")),
267        }
268    }
269}
270
271/// Feature category for grouping related features.
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
273#[serde(rename_all = "snake_case")]
274#[non_exhaustive]
275pub enum FeatureCategory {
276    /// Pre-release features under active development.
277    Experimental,
278    /// Compliance and audit-related features for regulated environments.
279    Enterprise,
280    /// Optimization features for throughput and memory.
281    Performance,
282    /// Diagnostic and profiling features for development.
283    Debug,
284    /// Testing infrastructure hooks (mutation, fuzzing, coverage).
285    Testing,
286}
287
288impl fmt::Display for FeatureCategory {
289    #[inline]
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        match self {
292            FeatureCategory::Experimental => write!(f, "experimental"),
293            FeatureCategory::Enterprise => write!(f, "enterprise"),
294            FeatureCategory::Performance => write!(f, "performance"),
295            FeatureCategory::Debug => write!(f, "debug"),
296            FeatureCategory::Testing => write!(f, "testing"),
297        }
298    }
299}
300
301/// Feature lifecycle stage.
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
303#[serde(rename_all = "snake_case")]
304#[non_exhaustive]
305pub enum FeatureLifecycle {
306    /// Feature is under active development and may change.
307    Experimental,
308    /// Feature is production-ready with stable API.
309    Stable,
310    /// Feature is scheduled for removal in a future release.
311    Deprecated,
312}
313
314impl fmt::Display for FeatureLifecycle {
315    #[inline]
316    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317        match self {
318            FeatureLifecycle::Experimental => write!(f, "experimental"),
319            FeatureLifecycle::Stable => write!(f, "stable"),
320            FeatureLifecycle::Deprecated => write!(f, "deprecated"),
321        }
322    }
323}
324
325/// Feature flag configuration.
326#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct FeatureFlags {
328    enabled: HashSet<Feature>,
329}
330
331impl Default for FeatureFlags {
332    #[inline]
333    fn default() -> Self {
334        let mut flags = Self {
335            enabled: HashSet::new(),
336        };
337
338        for feature in all_features() {
339            if feature.default_enabled() {
340                flags.enabled.insert(feature);
341            }
342        }
343
344        flags
345    }
346}
347
348impl FeatureFlags {
349    /// Get the global feature flags instance.
350    #[inline]
351    #[must_use]
352    pub fn global() -> &'static Self {
353        GLOBAL_FLAGS.get_or_init(Self::from_env)
354    }
355
356    /// Set the global feature flags.
357    #[inline]
358    pub fn set_global(flags: Self) {
359        let _ = GLOBAL_FLAGS.set(flags);
360    }
361
362    /// Create feature flags from environment variables.
363    #[inline]
364    #[must_use]
365    pub fn from_env() -> Self {
366        let mut flags = Self::default();
367
368        for (key, value) in env::vars() {
369            if let Some(feature_name) = key.strip_prefix("COPYBOOK_FF_")
370                && let Ok(feature) = Feature::from_str(feature_name)
371            {
372                let enabled = matches!(
373                    value.to_lowercase().as_str(),
374                    "1" | "true" | "yes" | "on" | "enabled"
375                );
376                if enabled {
377                    flags.enabled.insert(feature);
378                } else {
379                    flags.enabled.remove(&feature);
380                }
381            }
382        }
383
384        flags
385    }
386
387    /// Check whether a specific feature is currently enabled.
388    #[inline]
389    #[must_use]
390    pub fn is_enabled(&self, feature: Feature) -> bool {
391        self.enabled.contains(&feature)
392    }
393
394    /// Enable a feature flag.
395    #[inline]
396    pub fn enable(&mut self, feature: Feature) {
397        self.enabled.insert(feature);
398    }
399
400    /// Disable a feature flag.
401    #[inline]
402    pub fn disable(&mut self, feature: Feature) {
403        self.enabled.remove(&feature);
404    }
405
406    /// Toggle a feature flag between enabled and disabled.
407    #[inline]
408    pub fn toggle(&mut self, feature: Feature) {
409        if self.enabled.contains(&feature) {
410            self.enabled.remove(&feature);
411        } else {
412            self.enabled.insert(feature);
413        }
414    }
415
416    /// Iterate over all currently enabled features.
417    #[inline]
418    pub fn enabled_features(&self) -> impl Iterator<Item = &Feature> {
419        self.enabled.iter()
420    }
421
422    /// Return all enabled features belonging to a specific category.
423    #[inline]
424    #[must_use]
425    pub fn enabled_in_category(&self, category: FeatureCategory) -> Vec<Feature> {
426        self.enabled
427            .iter()
428            .filter(|f| f.category() == category)
429            .copied()
430            .collect()
431    }
432
433    /// Return all features (enabled or not) that belong to a category.
434    #[inline]
435    #[must_use]
436    pub fn features_in_category(category: FeatureCategory) -> Vec<Feature> {
437        all_features()
438            .into_iter()
439            .filter(|f| f.category() == category)
440            .collect()
441    }
442
443    /// Create a new [`FeatureFlagsBuilder`] starting from defaults.
444    #[inline]
445    #[must_use]
446    pub fn builder() -> FeatureFlagsBuilder {
447        FeatureFlagsBuilder::default()
448    }
449}
450
451/// Builder for constructing [`FeatureFlags`] with a fluent API.
452#[derive(Debug, Clone, Default)]
453pub struct FeatureFlagsBuilder {
454    flags: FeatureFlags,
455}
456
457impl FeatureFlagsBuilder {
458    /// Enable a single feature flag.
459    #[inline]
460    #[must_use]
461    pub fn enable(mut self, feature: Feature) -> Self {
462        self.flags.enable(feature);
463        self
464    }
465
466    /// Disable a single feature flag.
467    #[inline]
468    #[must_use]
469    pub fn disable(mut self, feature: Feature) -> Self {
470        self.flags.disable(feature);
471        self
472    }
473
474    /// Enable all features in a category.
475    #[inline]
476    #[must_use]
477    pub fn enable_category(mut self, category: FeatureCategory) -> Self {
478        for feature in FeatureFlags::features_in_category(category) {
479            self.flags.enable(feature);
480        }
481        self
482    }
483
484    /// Disable all features in a category.
485    #[inline]
486    #[must_use]
487    pub fn disable_category(mut self, category: FeatureCategory) -> Self {
488        for feature in FeatureFlags::features_in_category(category) {
489            self.flags.disable(feature);
490        }
491        self
492    }
493
494    /// Consume the builder and return the configured [`FeatureFlags`].
495    #[inline]
496    #[must_use]
497    pub fn build(self) -> FeatureFlags {
498        self.flags
499    }
500}
501
502static GLOBAL_FLAGS: OnceLock<FeatureFlags> = OnceLock::new();
503
504/// Thread-safe, mutable handle to [`FeatureFlags`] for runtime toggling.
505#[derive(Debug)]
506pub struct FeatureFlagsHandle {
507    flags: RwLock<FeatureFlags>,
508}
509
510impl Default for FeatureFlagsHandle {
511    #[inline]
512    fn default() -> Self {
513        Self {
514            flags: RwLock::new(FeatureFlags::from_env()),
515        }
516    }
517}
518
519impl FeatureFlagsHandle {
520    /// Create a new handle initialized from environment variables.
521    #[inline]
522    #[must_use]
523    pub fn new() -> Self {
524        Self::default()
525    }
526
527    /// Check whether a feature is enabled in this handle.
528    #[inline]
529    #[must_use]
530    pub fn is_enabled(&self, feature: Feature) -> bool {
531        self.flags
532            .read()
533            .map(|flags| flags.is_enabled(feature))
534            .unwrap_or(false)
535    }
536
537    /// Enable a feature flag through the handle.
538    #[inline]
539    pub fn enable(&self, feature: Feature) {
540        if let Ok(mut flags) = self.flags.write() {
541            flags.enable(feature);
542        }
543    }
544
545    /// Disable a feature flag through the handle.
546    #[inline]
547    pub fn disable(&self, feature: Feature) {
548        if let Ok(mut flags) = self.flags.write() {
549            flags.disable(feature);
550        }
551    }
552
553    /// Toggle a feature flag through the handle.
554    #[inline]
555    pub fn toggle(&self, feature: Feature) {
556        if let Ok(mut flags) = self.flags.write() {
557            flags.toggle(feature);
558        }
559    }
560
561    /// Take an immutable snapshot of the current feature flags.
562    #[inline]
563    #[must_use]
564    pub fn snapshot(&self) -> FeatureFlags {
565        self.flags
566            .read()
567            .map(|flags| flags.clone())
568            .unwrap_or_default()
569    }
570}
571
572impl Clone for FeatureFlagsHandle {
573    #[inline]
574    fn clone(&self) -> Self {
575        Self {
576            flags: RwLock::new(self.snapshot()),
577        }
578    }
579}
580
581/// Return a list of every defined [`Feature`] variant.
582#[inline]
583#[must_use]
584pub fn all_features() -> Vec<Feature> {
585    vec![
586        Feature::SignSeparate,
587        Feature::RenamesR4R6,
588        Feature::Comp1,
589        Feature::Comp2,
590        Feature::AuditSystem,
591        Feature::SoxCompliance,
592        Feature::HipaaCompliance,
593        Feature::GdprCompliance,
594        Feature::PciDssCompliance,
595        Feature::SecurityMonitoring,
596        Feature::AdvancedOptimization,
597        Feature::LruCache,
598        Feature::ParallelDecode,
599        Feature::ZeroCopy,
600        Feature::VerboseLogging,
601        Feature::DiagnosticOutput,
602        Feature::Profiling,
603        Feature::MemoryTracking,
604        Feature::MutationTesting,
605        Feature::FuzzingIntegration,
606        Feature::CoverageInstrumentation,
607        Feature::PropertyBasedTesting,
608    ]
609}
610
611#[cfg(test)]
612#[allow(clippy::expect_used)]
613#[allow(clippy::unwrap_used)]
614mod tests {
615    use super::*;
616
617    #[test]
618    fn test_feature_display() {
619        assert_eq!(Feature::SignSeparate.to_string(), "sign_separate");
620        assert_eq!(Feature::LruCache.to_string(), "lru_cache");
621    }
622
623    #[test]
624    fn test_feature_from_str() {
625        assert_eq!(
626            Feature::from_str("sign_separate").unwrap(),
627            Feature::SignSeparate
628        );
629        assert_eq!(Feature::from_str("LRU_CACHE").unwrap(), Feature::LruCache);
630        assert!(Feature::from_str("unknown_feature").is_err());
631    }
632
633    #[test]
634    fn test_feature_category() {
635        assert_eq!(
636            Feature::SignSeparate.category(),
637            FeatureCategory::Experimental
638        );
639        assert_eq!(Feature::AuditSystem.category(), FeatureCategory::Enterprise);
640        assert_eq!(Feature::LruCache.category(), FeatureCategory::Performance);
641        assert_eq!(Feature::VerboseLogging.category(), FeatureCategory::Debug);
642        assert_eq!(
643            Feature::MutationTesting.category(),
644            FeatureCategory::Testing
645        );
646    }
647
648    #[test]
649    fn test_default_enabled() {
650        assert!(Feature::SignSeparate.default_enabled());
651        assert!(Feature::Comp1.default_enabled());
652        assert!(Feature::Comp2.default_enabled());
653        assert!(Feature::LruCache.default_enabled());
654        assert!(!Feature::VerboseLogging.default_enabled());
655    }
656
657    #[test]
658    fn test_feature_flags_default() {
659        let flags = FeatureFlags::default();
660        assert!(flags.is_enabled(Feature::LruCache));
661        assert!(flags.is_enabled(Feature::SignSeparate));
662        assert!(flags.is_enabled(Feature::Comp1));
663        assert!(flags.is_enabled(Feature::Comp2));
664    }
665
666    #[test]
667    fn test_feature_flags_enable_disable() {
668        let mut flags = FeatureFlags::default();
669        flags.enable(Feature::SignSeparate);
670        assert!(flags.is_enabled(Feature::SignSeparate));
671        flags.disable(Feature::SignSeparate);
672        assert!(!flags.is_enabled(Feature::SignSeparate));
673    }
674
675    #[test]
676    fn test_feature_flags_toggle() {
677        let mut flags = FeatureFlags::default();
678        flags.toggle(Feature::LruCache);
679        assert!(!flags.is_enabled(Feature::LruCache));
680        flags.toggle(Feature::LruCache);
681        assert!(flags.is_enabled(Feature::LruCache));
682    }
683
684    #[test]
685    fn test_feature_flags_builder() {
686        let flags = FeatureFlags::builder()
687            .enable(Feature::SignSeparate)
688            .disable(Feature::LruCache)
689            .build();
690        assert!(flags.is_enabled(Feature::SignSeparate));
691        assert!(!flags.is_enabled(Feature::LruCache));
692    }
693
694    #[test]
695    fn test_feature_flags_enable_category() {
696        let flags = FeatureFlags::builder()
697            .enable_category(FeatureCategory::Experimental)
698            .build();
699        assert!(flags.is_enabled(Feature::SignSeparate));
700        assert!(flags.is_enabled(Feature::RenamesR4R6));
701        assert!(flags.is_enabled(Feature::Comp1));
702        assert!(flags.is_enabled(Feature::Comp2));
703    }
704
705    #[test]
706    fn test_feature_flags_handle() {
707        let handle = FeatureFlagsHandle::new();
708        handle.enable(Feature::SignSeparate);
709        assert!(handle.is_enabled(Feature::SignSeparate));
710        handle.disable(Feature::SignSeparate);
711        assert!(!handle.is_enabled(Feature::SignSeparate));
712        handle.enable(Feature::SignSeparate);
713        assert!(handle.is_enabled(Feature::SignSeparate));
714    }
715
716    #[test]
717    fn test_all_features() {
718        let features = all_features();
719        assert!(features.contains(&Feature::SignSeparate));
720        assert!(features.contains(&Feature::LruCache));
721        assert!(features.contains(&Feature::VerboseLogging));
722    }
723
724    #[test]
725    fn test_enabled_in_category() {
726        let mut flags = FeatureFlags::default();
727        flags.enable(Feature::RenamesR4R6);
728        let experimental = flags.enabled_in_category(FeatureCategory::Experimental);
729        assert_eq!(experimental.len(), 4);
730        assert!(experimental.contains(&Feature::SignSeparate));
731        assert!(experimental.contains(&Feature::Comp1));
732        assert!(experimental.contains(&Feature::Comp2));
733        assert!(experimental.contains(&Feature::RenamesR4R6));
734    }
735
736    #[test]
737    fn test_env_var_name() {
738        assert_eq!(
739            Feature::SignSeparate.env_var_name(),
740            "COPYBOOK_FF_SIGN_SEPARATE"
741        );
742        assert_eq!(Feature::LruCache.env_var_name(), "COPYBOOK_FF_LRU_CACHE");
743    }
744
745    #[test]
746    fn test_feature_serde_json_roundtrip() {
747        let feature = Feature::ParallelDecode;
748        let json = serde_json::to_string(&feature).unwrap();
749        let back: Feature = serde_json::from_str(&json).unwrap();
750        assert_eq!(back, feature);
751    }
752
753    #[test]
754    fn test_feature_category_serde_roundtrip() {
755        let cat = FeatureCategory::Enterprise;
756        let json = serde_json::to_string(&cat).unwrap();
757        let back: FeatureCategory = serde_json::from_str(&json).unwrap();
758        assert_eq!(back, cat);
759    }
760
761    #[test]
762    fn test_feature_lifecycle_display() {
763        assert_eq!(FeatureLifecycle::Experimental.to_string(), "experimental");
764        assert_eq!(FeatureLifecycle::Stable.to_string(), "stable");
765        assert_eq!(FeatureLifecycle::Deprecated.to_string(), "deprecated");
766    }
767
768    #[test]
769    fn test_feature_category_display() {
770        assert_eq!(FeatureCategory::Experimental.to_string(), "experimental");
771        assert_eq!(FeatureCategory::Enterprise.to_string(), "enterprise");
772        assert_eq!(FeatureCategory::Performance.to_string(), "performance");
773        assert_eq!(FeatureCategory::Debug.to_string(), "debug");
774        assert_eq!(FeatureCategory::Testing.to_string(), "testing");
775    }
776
777    #[test]
778    fn test_feature_flags_all_disabled() {
779        let mut flags = FeatureFlags::default();
780        for feature in all_features() {
781            flags.disable(feature);
782        }
783        for feature in all_features() {
784            assert!(!flags.is_enabled(feature), "{feature} should be disabled");
785        }
786    }
787
788    #[test]
789    fn test_feature_flags_all_enabled() {
790        let mut flags = FeatureFlags::default();
791        for feature in all_features() {
792            flags.enable(feature);
793        }
794        for feature in all_features() {
795            assert!(flags.is_enabled(feature), "{feature} should be enabled");
796        }
797    }
798
799    #[test]
800    fn test_builder_disable_category() {
801        let flags = FeatureFlags::builder()
802            .enable_category(FeatureCategory::Debug)
803            .disable_category(FeatureCategory::Debug)
804            .build();
805        let debug_features = flags.enabled_in_category(FeatureCategory::Debug);
806        assert!(debug_features.is_empty());
807    }
808
809    #[test]
810    fn test_handle_toggle_and_snapshot() {
811        let handle = FeatureFlagsHandle::new();
812        let initially_enabled = handle.is_enabled(Feature::LruCache);
813        handle.toggle(Feature::LruCache);
814        assert_ne!(handle.is_enabled(Feature::LruCache), initially_enabled);
815        let snap = handle.snapshot();
816        assert_ne!(snap.is_enabled(Feature::LruCache), initially_enabled);
817    }
818
819    #[test]
820    fn test_handle_clone_is_independent() {
821        let handle = FeatureFlagsHandle::new();
822        handle.enable(Feature::Profiling);
823        let cloned = handle.clone();
824        handle.disable(Feature::Profiling);
825        assert!(cloned.is_enabled(Feature::Profiling));
826        assert!(!handle.is_enabled(Feature::Profiling));
827    }
828
829    #[test]
830    fn test_features_in_category_counts() {
831        let experimental = FeatureFlags::features_in_category(FeatureCategory::Experimental);
832        assert_eq!(experimental.len(), 4);
833        let enterprise = FeatureFlags::features_in_category(FeatureCategory::Enterprise);
834        assert_eq!(enterprise.len(), 6);
835        let performance = FeatureFlags::features_in_category(FeatureCategory::Performance);
836        assert_eq!(performance.len(), 4);
837        let debug = FeatureFlags::features_in_category(FeatureCategory::Debug);
838        assert_eq!(debug.len(), 4);
839        let testing = FeatureFlags::features_in_category(FeatureCategory::Testing);
840        assert_eq!(testing.len(), 4);
841    }
842
843    #[test]
844    fn test_enabled_features_iterator_count() {
845        let flags = FeatureFlags::default();
846        let count = flags.enabled_features().count();
847        // Default-enabled: SignSeparate, Comp1, Comp2, LruCache
848        assert_eq!(count, 4);
849    }
850
851    #[test]
852    fn test_description_nonempty_for_all_features() {
853        for feature in all_features() {
854            assert!(
855                !feature.description().is_empty(),
856                "{feature} has empty description"
857            );
858        }
859    }
860
861    #[test]
862    fn test_from_str_unknown_returns_err() {
863        assert!(Feature::from_str("does_not_exist").is_err());
864        assert!(Feature::from_str("").is_err());
865    }
866
867    #[test]
868    fn test_feature_flags_serde_json_roundtrip() {
869        let flags = FeatureFlags::builder()
870            .enable(Feature::Profiling)
871            .disable(Feature::LruCache)
872            .build();
873        let json = serde_json::to_string(&flags).unwrap();
874        let back: FeatureFlags = serde_json::from_str(&json).unwrap();
875        assert!(back.is_enabled(Feature::Profiling));
876        assert!(!back.is_enabled(Feature::LruCache));
877    }
878}