Skip to main content

kcl_lib/
runtime_flags.rs

1use std::sync::RwLock;
2
3use serde::Deserialize;
4use serde::Serialize;
5
6/// Runtime representation for feature flags that can be set by the TS app.
7///
8/// TS currently provides a two-state feature answer: `true` means the feature is
9/// on, while `false` covers both explicit off/default behavior and a missing
10/// feature entry. Rust keeps a third state so code that was not initialized
11/// through the TS/wasm path can still fall back to Rust-side defaults, such as
12/// env-based configuration.
13#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)]
14#[ts(export)]
15pub enum RuntimeFlag {
16    /// No TS/wasm runtime flag has been installed; fall back to Rust defaults.
17    #[default]
18    Unset,
19    /// TS observed the feature as on; use the new feature behavior.
20    On,
21    /// TS observed the feature as false or missing; use default behavior.
22    Off,
23}
24
25/// Maps 1-1 to the KCL related flags added to the Admin portal and TS.
26///
27/// Fields missing from a deserialized payload become [`RuntimeFlag::Unset`],
28/// so a sender built before a flag existed falls back to Rust-side defaults
29/// instead of failing to parse.
30///
31/// `use_new_parser` is a placeholder while the new parser is being implemented:
32///
33/// - It has no corresponding Admin portal flag yet.
34/// - It defaults to [`RuntimeFlag::Off`] when omitted.
35/// - The current parser does not read it.
36#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)]
37#[serde(default)]
38#[ts(export)]
39pub struct KclRuntimeFlags {
40    pub use_new_parser: RuntimeFlag,
41}
42
43impl KclRuntimeFlags {
44    pub const DEFAULT: Self = Self {
45        use_new_parser: RuntimeFlag::Off,
46    };
47}
48
49impl Default for KclRuntimeFlags {
50    fn default() -> Self {
51        Self::DEFAULT
52    }
53}
54
55static KCL_RUNTIME_FLAGS: RwLock<KclRuntimeFlags> = RwLock::new(KclRuntimeFlags::DEFAULT);
56
57pub fn set_kcl_runtime_flags(flags: KclRuntimeFlags) {
58    match KCL_RUNTIME_FLAGS.write() {
59        Ok(mut guard) => *guard = flags,
60        Err(poisoned) => {
61            let mut guard = poisoned.into_inner();
62            *guard = flags;
63        }
64    }
65}
66
67pub fn kcl_runtime_flags() -> KclRuntimeFlags {
68    match KCL_RUNTIME_FLAGS.read() {
69        Ok(guard) => *guard,
70        Err(poisoned) => *poisoned.into_inner(),
71    }
72}
73
74#[allow(dead_code, reason = "Retained for future runtime flag consumers.")]
75pub(crate) trait RuntimeFlagResolve {
76    fn on() -> Self;
77    fn off() -> Self;
78    /// Not named `default()` so that it doesn't collide with
79    /// `Default::default()`.
80    fn resolve_default() -> Self;
81    fn parse_env_var(value: &str) -> Self;
82}
83
84#[allow(dead_code, reason = "Retained for future runtime flag consumers.")]
85pub(crate) fn resolve_from_sources<T: RuntimeFlagResolve>(
86    runtime_flag: RuntimeFlag,
87    test_override: Option<T>,
88    env_value: Option<&str>,
89) -> T {
90    match runtime_flag {
91        RuntimeFlag::On => return T::on(),
92        RuntimeFlag::Off => return T::off(),
93        RuntimeFlag::Unset => {}
94    }
95
96    if let Some(mode) = test_override {
97        return mode;
98    }
99
100    env_value.map(T::parse_env_var).unwrap_or_else(T::resolve_default)
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn deserializing_empty_flags_preserves_rust_defaults() {
109        let flags: KclRuntimeFlags = serde_json::from_str("{}").unwrap();
110        assert_eq!(flags.use_new_parser, RuntimeFlag::Off);
111    }
112
113    #[test]
114    fn deserializing_explicit_parser_flag_preserves_its_value() {
115        let flags: KclRuntimeFlags = serde_json::from_str(r#"{"use_new_parser":"On"}"#).unwrap();
116        assert_eq!(flags.use_new_parser, RuntimeFlag::On);
117    }
118
119    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
120    enum TestMode {
121        Current,
122        New,
123    }
124
125    impl RuntimeFlagResolve for TestMode {
126        fn on() -> Self {
127            Self::New
128        }
129
130        fn off() -> Self {
131            Self::Current
132        }
133
134        fn resolve_default() -> Self {
135            Self::Current
136        }
137
138        fn parse_env_var(value: &str) -> Self {
139            match value {
140                "new" => Self::New,
141                _ => Self::Current,
142            }
143        }
144    }
145
146    #[test]
147    fn runtime_flag_takes_priority_over_test_override_and_env() {
148        assert_eq!(
149            resolve_from_sources(RuntimeFlag::Off, Some(TestMode::New), Some("new")),
150            TestMode::Current
151        );
152        assert_eq!(
153            resolve_from_sources(RuntimeFlag::On, Some(TestMode::Current), Some("current")),
154            TestMode::New
155        );
156    }
157
158    #[test]
159    fn test_override_takes_priority_over_env() {
160        assert_eq!(
161            resolve_from_sources(RuntimeFlag::Unset, Some(TestMode::Current), Some("new")),
162            TestMode::Current
163        );
164        assert_eq!(
165            resolve_from_sources(RuntimeFlag::Unset, Some(TestMode::New), Some("current")),
166            TestMode::New
167        );
168    }
169
170    #[test]
171    fn unset_runtime_flag_allows_env_to_select_mode() {
172        assert_eq!(
173            resolve_from_sources::<TestMode>(RuntimeFlag::Unset, None, Some("new")),
174            TestMode::New
175        );
176        assert_eq!(
177            resolve_from_sources::<TestMode>(RuntimeFlag::Unset, None, Some("current")),
178            TestMode::Current
179        );
180    }
181
182    #[test]
183    fn unset_runtime_flag_and_missing_env_select_default_mode() {
184        assert_eq!(
185            resolve_from_sources::<TestMode>(RuntimeFlag::Unset, None, None),
186            TestMode::Current
187        );
188    }
189}