1use sofie_core::{Error, Result};
2use sofie_flags::{Feature, Features, Flags, Rule, Subject};
3
4use super::Gates;
5use crate::answer::Response;
6use crate::caller::Caller;
7use crate::routes::Route;
8use crate::session::RequestSession;
9use crate::target::Target;
10use crate::{Area, RequestContext};
11use sofie_db::Resolve;
12
13pub struct RouteFeature {
14 pub method: &'static str,
15 pub path: &'static str,
16 pub feature: Feature,
17}
18
19#[derive(Default)]
20pub(super) struct FlagGate {
21 flags: Option<Flags>,
22 gated: &'static [RouteFeature],
23}
24
25impl Gates {
26 #[must_use]
27 pub fn features(mut self, flags: Flags) -> Self {
28 self.flags.flags = Some(flags);
29 self
30 }
31
32 #[must_use]
34 pub fn route_features(mut self, gated: &'static [RouteFeature]) -> Self {
35 self.flags.gated = gated;
36 self
37 }
38}
39
40impl FlagGate {
41 pub(super) fn held(&self) -> Option<Flags> {
42 self.flags.clone()
43 }
44
45 pub(super) fn prepare(&self, routes: &mut [Route]) -> Result<()> {
46 for gate in self.gated {
47 let route = routes
48 .iter_mut()
49 .find(|route| route.method.as_str() == gate.method && route.path == gate.path)
50 .ok_or_else(|| {
51 Error::internal_message(format!(
52 "the wiring gates route {} {}, which no module routes; run `sofie gen`",
53 gate.method, gate.path
54 ))
55 })?;
56 route.rules.feature = Some(gate.feature);
57 }
58 let flagged = routes.iter().find(|route| route.rules.feature.is_some());
59 if let Some(route) = flagged.filter(|_| self.flags.is_none()) {
60 return Err(no_flags(&format!(
61 "route {} {} has a feature flag",
62 route.method, route.path
63 )));
64 }
65 Ok(())
66 }
67
68 pub(super) fn open(
70 &self,
71 target: &Target,
72 session: &RequestSession,
73 caller: Caller<'_>,
74 ) -> Result<Option<Features>, Box<Response>> {
75 let Some(flags) = &self.flags else {
76 return Ok(None);
77 };
78 let area = target.audience.name;
79 let features = flags.evaluate(Subject {
80 area: Some(area),
81 principal: session.principal_in(area),
82 });
83 #[cfg(all(feature = "toolbar", debug_assertions))]
84 let features = features.overridden(std::sync::Arc::clone(&session.toolbar.flags));
85 let evaluated = features.clone();
86 sofie_telemetry::attach_feature_flags(&tracing::Span::current(), move || {
87 evaluated.evaluated()
88 });
89 if let Some(feature) = target.admission.rules.feature
90 && !features.enabled(feature)
91 {
92 tracing::info!(
93 feature_flag.key = feature.name(),
94 http.route = target.pattern.as_str(),
95 "not found: the route's feature is off"
96 );
97 return Err(Box::new(crate::answer::failure(Error::NotFound, caller)));
98 }
99 Ok(Some(features))
100 }
101}
102
103pub(super) fn record(features: Option<&Features>) {
104 let evaluated = features.map(Features::evaluated).unwrap_or_default();
105 if !evaluated.is_empty() {
106 tracing::Span::current().record("feature_flags", evaluated);
107 }
108}
109
110impl<A: Area, M> Resolve<RequestContext<A, M>> for Features {
111 fn resolve(context: &mut RequestContext<A, M>) -> impl Future<Output = Result<Self>> + Send {
112 let features = context
113 .matched
114 .gates
115 .features
116 .clone()
117 .ok_or_else(|| no_flags("a handler asks for `Features`"));
118 std::future::ready(features)
119 }
120}
121
122fn flags<A: Area, M>(context: &RequestContext<A, M>) -> Result<Flags> {
123 context
124 .matched
125 .gates
126 .flags
127 .clone()
128 .ok_or_else(|| no_flags("a handler asks for `Flags`"))
129}
130
131fn no_flags(asking: &str) -> Error {
132 Error::internal_message(format!(
133 "{asking}, but the app has no flags; give `Gates::features` the app's `Flags`"
134 ))
135}
136
137impl<A: Area> Resolve<RequestContext<A>> for Flags {
138 fn resolve(context: &mut RequestContext<A>) -> impl Future<Output = Result<Self>> + Send {
139 std::future::ready(flags(context))
140 }
141}
142
143pub struct FlagRules(Flags);
145
146impl FlagRules {
147 #[must_use]
148 pub fn rules(&self) -> Vec<(String, Rule)> {
149 self.0.rules()
150 }
151}
152
153impl<A: Area, M> Resolve<RequestContext<A, M>> for FlagRules {
154 fn resolve(context: &mut RequestContext<A, M>) -> impl Future<Output = Result<Self>> + Send {
155 std::future::ready(flags(context).map(Self))
156 }
157}