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]
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 #[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 #[must_use]
114 pub fn settings(&self) -> Vec<HeldSetting> {
115 self.snapshot.held_settings()
116 }
117
118 #[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}