Skip to main content

videocall_types/
feature_flags.rs

1/*
2 * Copyright 2025 Security Union LLC
3 *
4 * Licensed under either of
5 *
6 * * Apache License, Version 2.0
7 *   (http://www.apache.org/licenses/LICENSE-2.0)
8 * * MIT license
9 *   (http://opensource.org/licenses/MIT)
10 *
11 * at your option.
12 */
13
14//! Feature flags for videocall-rs.
15//!
16//! Flags are loaded lazily from environment variables on first access.
17//! Add new flags here as the project evolves.
18
19use std::sync::atomic::{AtomicU8, Ordering};
20use std::sync::OnceLock;
21
22/// Environment variable prefix for feature flags
23const ENV_PREFIX: &str = "FEATURE_";
24
25/// Override states for testing
26const OVERRIDE_NONE: u8 = 0;
27const OVERRIDE_TRUE: u8 = 1;
28const OVERRIDE_FALSE: u8 = 2;
29
30/// Test override for meeting_management flag
31static MEETING_MANAGEMENT_OVERRIDE: AtomicU8 = AtomicU8::new(OVERRIDE_NONE);
32
33/// Feature flags singleton, lazily initialized from environment variables.
34#[derive(Debug, Clone)]
35pub struct FeatureFlags {
36    /// Enable meeting lifecycle management (creation, tracking, host controls).
37    /// Env: FEATURE_MEETING_MANAGEMENT=true
38    pub meeting_management: bool,
39
40    /// Enable database connections (PostgreSQL).
41    /// Env: DATABASE_ENABLED=true
42    pub database: bool,
43}
44
45impl FeatureFlags {
46    /// Load feature flags from environment variables.
47    fn from_env() -> Self {
48        Self {
49            meeting_management: read_bool_env("MEETING_MANAGEMENT"),
50            database: read_bool_no_prefix("DATABASE_ENABLED"),
51        }
52    }
53
54    /// Get the global feature flags instance.
55    /// Lazily initialized on first call.
56    pub fn global() -> &'static Self {
57        static FLAGS: OnceLock<FeatureFlags> = OnceLock::new();
58        FLAGS.get_or_init(FeatureFlags::from_env)
59    }
60
61    /// Check if meeting management is enabled.
62    /// Respects test overrides if set.
63    #[inline]
64    pub fn meeting_management_enabled() -> bool {
65        match MEETING_MANAGEMENT_OVERRIDE.load(Ordering::SeqCst) {
66            OVERRIDE_TRUE => true,
67            OVERRIDE_FALSE => false,
68            _ => Self::global().meeting_management,
69        }
70    }
71
72    /// Check if database is enabled.
73    #[inline]
74    pub fn database_enabled() -> bool {
75        Self::global().database
76    }
77
78    /// Override meeting_management flag for testing.
79    /// Call `clear_meeting_management_override()` to restore normal behavior.
80    ///
81    /// Only available with the `testing` feature enabled.
82    #[cfg(any(test, feature = "testing"))]
83    pub fn set_meeting_management_override(enabled: bool) {
84        let value = if enabled {
85            OVERRIDE_TRUE
86        } else {
87            OVERRIDE_FALSE
88        };
89        MEETING_MANAGEMENT_OVERRIDE.store(value, Ordering::SeqCst);
90    }
91
92    /// Clear the meeting_management override, restoring env-based behavior.
93    ///
94    /// Only available with the `testing` feature enabled.
95    #[cfg(any(test, feature = "testing"))]
96    pub fn clear_meeting_management_override() {
97        MEETING_MANAGEMENT_OVERRIDE.store(OVERRIDE_NONE, Ordering::SeqCst);
98    }
99}
100
101/// Read a boolean environment variable with the FEATURE_ prefix.
102/// Returns false if not set or not a truthy value.
103fn read_bool_env(name: &str) -> bool {
104    let full_name = format!("{ENV_PREFIX}{name}");
105    std::env::var(&full_name)
106        .map(|v| matches!(v.to_lowercase().as_str(), "true" | "1" | "yes"))
107        .unwrap_or(false)
108}
109
110/// Read a boolean environment variable without prefix (for legacy env vars like DATABASE_ENABLED).
111/// Returns false if not set or not a truthy value.
112fn read_bool_no_prefix(name: &str) -> bool {
113    std::env::var(name)
114        .map(|v| matches!(v.to_lowercase().as_str(), "true" | "1" | "yes"))
115        .unwrap_or(false)
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_read_bool_env_not_set() {
124        // Ensure the env var is not set
125        std::env::remove_var("FEATURE_TEST_FLAG");
126        assert!(!read_bool_env("TEST_FLAG"));
127    }
128
129    #[test]
130    fn test_read_bool_env_truthy_values() {
131        std::env::set_var("FEATURE_TEST_TRUE", "true");
132        assert!(read_bool_env("TEST_TRUE"));
133
134        std::env::set_var("FEATURE_TEST_ONE", "1");
135        assert!(read_bool_env("TEST_ONE"));
136
137        std::env::set_var("FEATURE_TEST_YES", "yes");
138        assert!(read_bool_env("TEST_YES"));
139
140        std::env::set_var("FEATURE_TEST_TRUE_UPPER", "TRUE");
141        assert!(read_bool_env("TEST_TRUE_UPPER"));
142
143        // Cleanup
144        std::env::remove_var("FEATURE_TEST_TRUE");
145        std::env::remove_var("FEATURE_TEST_ONE");
146        std::env::remove_var("FEATURE_TEST_YES");
147        std::env::remove_var("FEATURE_TEST_TRUE_UPPER");
148    }
149
150    #[test]
151    fn test_read_bool_env_falsy_values() {
152        std::env::set_var("FEATURE_TEST_FALSE", "false");
153        assert!(!read_bool_env("TEST_FALSE"));
154
155        std::env::set_var("FEATURE_TEST_ZERO", "0");
156        assert!(!read_bool_env("TEST_ZERO"));
157
158        std::env::set_var("FEATURE_TEST_RANDOM", "random");
159        assert!(!read_bool_env("TEST_RANDOM"));
160
161        // Cleanup
162        std::env::remove_var("FEATURE_TEST_FALSE");
163        std::env::remove_var("FEATURE_TEST_ZERO");
164        std::env::remove_var("FEATURE_TEST_RANDOM");
165    }
166}