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#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)]
31#[ts(export)]
32pub struct KclRuntimeFlags {
33    #[serde(default)]
34    pub use_cek_executor: RuntimeFlag,
35    #[serde(default)]
36    pub use_new_lexer_parser: RuntimeFlag,
37}
38
39impl KclRuntimeFlags {
40    pub const DEFAULT: Self = Self {
41        use_cek_executor: RuntimeFlag::Unset,
42        use_new_lexer_parser: RuntimeFlag::Unset,
43    };
44}
45
46impl Default for KclRuntimeFlags {
47    fn default() -> Self {
48        Self::DEFAULT
49    }
50}
51
52static KCL_RUNTIME_FLAGS: RwLock<KclRuntimeFlags> = RwLock::new(KclRuntimeFlags::DEFAULT);
53
54pub fn set_kcl_runtime_flags(flags: KclRuntimeFlags) {
55    match KCL_RUNTIME_FLAGS.write() {
56        Ok(mut guard) => *guard = flags,
57        Err(poisoned) => {
58            let mut guard = poisoned.into_inner();
59            *guard = flags;
60        }
61    }
62}
63
64pub fn kcl_runtime_flags() -> KclRuntimeFlags {
65    match KCL_RUNTIME_FLAGS.read() {
66        Ok(guard) => *guard,
67        Err(poisoned) => *poisoned.into_inner(),
68    }
69}
70
71pub(crate) trait RuntimeFlagResolve {
72    fn on() -> Self;
73    fn off() -> Self;
74    /// Not named `default()` so that it doesn't collide with
75    /// `Default::default()`.
76    fn resolve_default() -> Self;
77    fn parse_env_var(value: &str) -> Self;
78}
79
80pub(crate) fn resolve_from_sources<T: RuntimeFlagResolve>(
81    runtime_flag: RuntimeFlag,
82    test_override: Option<T>,
83    env_value: Option<&str>,
84) -> T {
85    match runtime_flag {
86        RuntimeFlag::On => return T::on(),
87        RuntimeFlag::Off => return T::off(),
88        RuntimeFlag::Unset => {}
89    }
90
91    if let Some(mode) = test_override {
92        return mode;
93    }
94
95    env_value.map(T::parse_env_var).unwrap_or_else(T::resolve_default)
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn deserializing_empty_flags_defaults_to_unset() {
104        let flags: KclRuntimeFlags = serde_json::from_str("{}").unwrap();
105        assert_eq!(flags, KclRuntimeFlags::DEFAULT);
106    }
107
108    #[test]
109    fn deserializing_partial_flags_defaults_missing_fields_to_unset() {
110        let flags: KclRuntimeFlags = serde_json::from_str(r#"{"use_new_lexer_parser":"On"}"#).unwrap();
111        assert_eq!(
112            flags,
113            KclRuntimeFlags {
114                use_cek_executor: RuntimeFlag::Unset,
115                use_new_lexer_parser: RuntimeFlag::On,
116            }
117        );
118    }
119}