Skip to main content

sofie_flags/
features.rs

1use std::sync::{Arc, Mutex, MutexGuard};
2
3use sofie_core::unpoisoned;
4
5use crate::flags::Snapshot;
6use crate::{Feature, HeldSetting, SettingValue, Subject, Variants};
7
8/// A flag nobody stored a rule for is off.
9#[derive(Clone)]
10pub struct Features {
11    snapshot: Arc<Snapshot>,
12    environment: Arc<str>,
13    subject: Subject,
14    evaluated: Arc<Mutex<Vec<(String, String)>>>,
15    #[cfg(all(feature = "toolbar", debug_assertions))]
16    overrides: Arc<[(String, String)]>,
17}
18
19impl Features {
20    pub(crate) fn new(snapshot: Arc<Snapshot>, environment: Arc<str>, subject: Subject) -> Self {
21        Self {
22            snapshot,
23            environment,
24            subject,
25            evaluated: Arc::default(),
26            #[cfg(all(feature = "toolbar", debug_assertions))]
27            overrides: Arc::new([]),
28        }
29    }
30
31    /// Each answer is `on`, `off` or a variant's name, and wins over the stored rule.
32    #[cfg(all(feature = "toolbar", debug_assertions))]
33    #[must_use]
34    pub fn overridden(self, overrides: Arc<[(String, String)]>) -> Self {
35        Self { overrides, ..self }
36    }
37
38    #[must_use]
39    pub fn enabled(&self, feature: Feature) -> bool {
40        self.enabled_named(feature.name())
41    }
42
43    #[must_use]
44    pub fn variant<V: sofie_core::Variants>(&self, variants: Variants<V>) -> Option<V> {
45        let name = variants.feature().name();
46        let chosen = match self.overriding(name) {
47            Some("off") => None,
48            Some("on") => self
49                .snapshot
50                .rules
51                .get(name)
52                .and_then(|rule| rule.variant(name, self.subject)),
53            Some(variant) => Some(variant),
54            None => self
55                .snapshot
56                .rules
57                .get(name)
58                .filter(|rule| rule.is_on(name, &self.environment, self.subject))
59                .and_then(|rule| rule.variant(name, self.subject)),
60        };
61        self.record(name, chosen.unwrap_or("off"));
62        let chosen = chosen?;
63        let variant = sofie_core::variants::parse::<V>(chosen);
64        if variant.is_none() {
65            tracing::warn!(
66                feature_flag.key = name,
67                feature_flag.variant = chosen,
68                "the flag's rule names a variant its enum does not have"
69            );
70        }
71        variant
72    }
73
74    #[must_use]
75    pub fn enabled_named(&self, name: &str) -> bool {
76        let on = match self.overriding(name) {
77            Some(answer) => answer != "off",
78            None => self
79                .snapshot
80                .rules
81                .get(name)
82                .is_some_and(|rule| rule.is_on(name, &self.environment, self.subject)),
83        };
84        self.record(name, if on { "on" } else { "off" });
85        on
86    }
87
88    /// Whether any principal of the subject's area could have it on: what a request that carries
89    /// no credentials, such as a CORS preflight, can be told. Not recorded.
90    #[must_use]
91    pub fn enabled_for_someone(&self, feature: Feature) -> bool {
92        let name = feature.name();
93        match self.overriding(name) {
94            Some(answer) => answer != "off",
95            None => {
96                self.snapshot.rules.get(name).is_some_and(|rule| {
97                    rule.is_on_for_someone(&self.environment, self.subject.area)
98                })
99            }
100        }
101    }
102
103    /// `default` unless a value of its kind is stored.
104    #[must_use]
105    pub fn setting(&self, name: &str, default: SettingValue) -> SettingValue {
106        match self.snapshot.settings.get(name) {
107            Some(stored) if stored.same_kind(&default) => stored.clone(),
108            _ => default,
109        }
110    }
111
112    /// Sorted by name.
113    #[must_use]
114    pub fn settings(&self) -> Vec<HeldSetting> {
115        self.snapshot.held_settings()
116    }
117
118    /// Changes whenever a rule or a setting does; equal on every instance holding the same ones.
119    #[must_use]
120    pub fn version(&self) -> u64 {
121        self.snapshot.version
122    }
123
124    #[must_use]
125    pub fn evaluated(&self) -> String {
126        let recorded = self.recorded();
127        let pairs: Vec<String> = recorded
128            .iter()
129            .map(|(name, answer)| format!("{name}={answer}"))
130            .collect();
131        pairs.join(" ")
132    }
133
134    #[cfg_attr(
135        not(all(feature = "toolbar", debug_assertions)),
136        expect(
137            clippy::unused_self,
138            reason = "only a debug build's toolbar overrides a flag"
139        )
140    )]
141    fn overriding(&self, name: &str) -> Option<&str> {
142        #[cfg(all(feature = "toolbar", debug_assertions))]
143        return self
144            .overrides
145            .iter()
146            .find(|(overridden, _)| overridden == name)
147            .map(|(_, answer)| answer.as_str());
148        #[cfg(not(all(feature = "toolbar", debug_assertions)))]
149        {
150            let _ = name;
151            None
152        }
153    }
154
155    fn record(&self, name: &str, answer: &str) {
156        let mut evaluated = self.recorded();
157        if !evaluated.iter().any(|(recorded, _)| recorded == name) {
158            evaluated.push((name.to_owned(), answer.to_owned()));
159        }
160    }
161
162    fn recorded(&self) -> MutexGuard<'_, Vec<(String, String)>> {
163        unpoisoned::lock(&self.evaluated)
164    }
165}
166
167#[cfg(all(test, feature = "toolbar", debug_assertions))]
168mod tests {
169    use super::*;
170    use crate::{Feature, Rule};
171
172    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
173    enum Layout {
174        Classic,
175        Compact,
176    }
177
178    impl sofie_core::Variants for Layout {
179        const TYPE: &'static str = "Layout";
180        const VARIANTS: &'static [(&'static str, Self)] =
181            &[("classic", Self::Classic), ("compact", Self::Compact)];
182    }
183
184    #[test]
185    fn an_override_wins_over_the_stored_rule() {
186        let mut snapshot = Snapshot::default();
187        snapshot
188            .rules
189            .insert("shop.beta".to_owned(), Rule::everyone());
190        let subject = Subject {
191            area: None,
192            principal: None,
193        };
194        let overrides = [
195            ("shop.beta", "off"),
196            ("shop.split", "on"),
197            ("shop.layout", "compact"),
198        ]
199        .map(|(name, answer)| (name.to_owned(), answer.to_owned()));
200
201        let features = Features::new(Arc::new(snapshot), Arc::from("test"), subject)
202            .overridden(Arc::new(overrides));
203
204        assert!(!features.enabled(Feature::new("shop.beta")));
205        assert!(features.enabled(Feature::new("shop.split")));
206        assert_eq!(
207            features.variant(Variants::<Layout>::new("shop.layout")),
208            Some(Layout::Compact)
209        );
210        assert_eq!(
211            features.evaluated(),
212            "shop.beta=off shop.split=on shop.layout=compact"
213        );
214    }
215}