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#[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 #[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 #[must_use]
90 pub fn setting(&self, name: &str, default: SettingValue) -> SettingValue {
91 match self.snapshot.settings.get(name) {
92 Some(stored) if stored.same_kind(&default) => stored.clone(),
93 _ => default,
94 }
95 }
96
97 #[must_use]
99 pub fn settings(&self) -> Vec<HeldSetting> {
100 self.snapshot.held_settings()
101 }
102
103 #[must_use]
105 pub fn version(&self) -> u64 {
106 self.snapshot.version
107 }
108
109 #[must_use]
110 pub fn evaluated(&self) -> String {
111 let recorded = self.recorded();
112 let pairs: Vec<String> = recorded
113 .iter()
114 .map(|(name, answer)| format!("{name}={answer}"))
115 .collect();
116 pairs.join(" ")
117 }
118
119 #[cfg_attr(
120 not(all(feature = "toolbar", debug_assertions)),
121 expect(
122 clippy::unused_self,
123 reason = "only a debug build's toolbar overrides a flag"
124 )
125 )]
126 fn overriding(&self, name: &str) -> Option<&str> {
127 #[cfg(all(feature = "toolbar", debug_assertions))]
128 return self
129 .overrides
130 .iter()
131 .find(|(overridden, _)| overridden == name)
132 .map(|(_, answer)| answer.as_str());
133 #[cfg(not(all(feature = "toolbar", debug_assertions)))]
134 {
135 let _ = name;
136 None
137 }
138 }
139
140 fn record(&self, name: &str, answer: &str) {
141 let mut evaluated = self.recorded();
142 if !evaluated.iter().any(|(recorded, _)| recorded == name) {
143 evaluated.push((name.to_owned(), answer.to_owned()));
144 }
145 }
146
147 fn recorded(&self) -> MutexGuard<'_, Vec<(String, String)>> {
148 unpoisoned::lock(&self.evaluated)
149 }
150}
151
152#[cfg(all(test, feature = "toolbar", debug_assertions))]
153mod tests {
154 use super::*;
155 use crate::{Feature, Rule};
156
157 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
158 enum Layout {
159 Classic,
160 Compact,
161 }
162
163 impl sofie_core::Variants for Layout {
164 const TYPE: &'static str = "Layout";
165 const VARIANTS: &'static [(&'static str, Self)] =
166 &[("classic", Self::Classic), ("compact", Self::Compact)];
167 }
168
169 #[test]
170 fn an_override_wins_over_the_stored_rule() {
171 let mut snapshot = Snapshot::default();
172 snapshot
173 .rules
174 .insert("shop.beta".to_owned(), Rule::everyone());
175 let subject = Subject {
176 area: None,
177 principal: None,
178 };
179 let overrides = [
180 ("shop.beta", "off"),
181 ("shop.split", "on"),
182 ("shop.layout", "compact"),
183 ]
184 .map(|(name, answer)| (name.to_owned(), answer.to_owned()));
185
186 let features = Features::new(Arc::new(snapshot), Arc::from("test"), subject)
187 .overridden(Arc::new(overrides));
188
189 assert!(!features.enabled(Feature::new("shop.beta")));
190 assert!(features.enabled(Feature::new("shop.split")));
191 assert_eq!(
192 features.variant(Variants::<Layout>::new("shop.layout")),
193 Some(Layout::Compact)
194 );
195 assert_eq!(
196 features.evaluated(),
197 "shop.beta=off shop.split=on shop.layout=compact"
198 );
199 }
200}