1use std::marker::PhantomData;
2
3use http::Method;
4use sofie_db::{Call, Reach};
5
6use crate::area::AreaPrincipal;
7use crate::handler::{Endpoint, Loader, endpoint, loader_endpoint};
8use crate::session::RequestSession;
9use crate::throttle::Rate;
10use crate::{Access, Area, RequestContext};
11
12pub(crate) const BODY_LIMIT: usize = 64 * 1024;
13
14#[derive(Clone)]
15pub(crate) enum View {
16 Json,
17 #[cfg(feature = "engine")]
18 Screen(ScreenView),
19 #[cfg(feature = "engine")]
20 Refresh(Refresh),
21 Redirect(String),
22 Follow,
24 RedirectOrFollow(String),
26 Document(Media),
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum Media {
32 Xml,
33 Text,
34 #[cfg(feature = "csv")]
36 Csv,
37}
38
39#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct Text(pub String);
42
43impl sofie_db::Outcome for Text {
44 fn reply(self) -> sofie_core::Result<sofie_db::Reply> {
45 Ok(sofie_db::Reply::Json(serde_json::Value::String(self.0)))
46 }
47}
48
49impl sofie_db::ReadOnly for Text {}
50
51impl Media {
52 pub(crate) fn content_type(self) -> &'static str {
53 match self {
54 Self::Xml => "application/xml; charset=utf-8",
55 Self::Text => "text/plain; charset=utf-8",
56 #[cfg(feature = "csv")]
57 Self::Csv => "text/csv; charset=utf-8",
58 }
59 }
60}
61
62#[cfg(feature = "engine")]
63#[derive(Clone)]
64pub(crate) struct ScreenView {
65 pub(crate) key: String,
66 pub(crate) title: String,
67 #[cfg(feature = "resources")]
68 pub(crate) builtin: Option<std::sync::Arc<crate::gate::optional::resource::Builtin>>,
69}
70
71#[cfg(feature = "engine")]
72#[derive(Clone)]
73pub(crate) struct Refresh {
74 pub(crate) screen: String,
75 pub(crate) fragment: String,
76}
77
78#[derive(Clone, Copy, Debug)]
79pub(crate) struct Audience {
80 pub(crate) name: &'static str,
81 pub(crate) area: std::any::TypeId,
83 pub(crate) access: Access,
84 pub(crate) sign_in: &'static str,
85 #[cfg(feature = "tokens")]
86 pub(crate) tokens: bool,
87}
88
89impl Audience {
90 fn of<A: Area>() -> Self {
91 Self {
92 name: A::NAME,
93 area: std::any::TypeId::of::<A>(),
94 access: <A::Principal as AreaPrincipal>::ACCESS,
95 sign_in: A::SIGN_IN,
96 #[cfg(feature = "tokens")]
97 tokens: A::TOKENS,
98 }
99 }
100
101 pub(crate) fn reach(self, session: &RequestSession) -> Option<Reach> {
103 match self.access {
104 Access::Public => Some(Reach::All),
105 Access::Owned => session.principal_in(self.name).map(Reach::OwnedBy),
106 #[cfg(feature = "permissions")]
107 Access::Permitted => session.principal_in(self.name).map(Reach::AllAs),
108 }
109 }
110}
111
112#[cfg(feature = "engine")]
113#[must_use]
114pub fn screen_key(module: &str, area: &str, name: &str) -> String {
115 format!("{module}/{area}/{name}")
116}
117
118pub(crate) struct Route {
119 pub(crate) module: &'static str,
120 pub(crate) name: Option<&'static str>,
121 pub(crate) method: Method,
122 pub(crate) path: String,
123 pub(crate) audience: Audience,
124 pub(crate) endpoint: Endpoint,
125 pub(crate) view: View,
126 pub(crate) rules: RouteRules,
127}
128
129impl Route {
130 pub(crate) fn name_as(&mut self, area: &str, handler: &str) {
132 let name = format!("{}.{area}.{handler}", self.module);
133 self.name = Some(Box::leak(name.into_boxed_str()));
134 }
135}
136
137#[derive(Clone, Default)]
138pub(crate) struct RouteRules {
139 pub(crate) throttle: Option<Rate>,
140 pub(crate) body_limit: Option<usize>,
141 #[cfg(feature = "uploads")]
142 pub(crate) file_limit: Option<usize>,
143 #[cfg(feature = "signed-links")]
144 pub(crate) signed: bool,
145 #[cfg(feature = "idempotency")]
146 pub(crate) idempotent: bool,
147 #[cfg(feature = "webhooks")]
148 pub(crate) verify: Option<std::sync::Arc<dyn crate::webhook::Verify>>,
149 #[cfg(feature = "flags")]
150 pub(crate) feature: Option<sofie_flags::Feature>,
151 #[cfg(feature = "entitlements")]
152 pub(crate) entitlement: Option<crate::Entitlement>,
153 #[cfg(feature = "permissions")]
155 pub(crate) permission: Option<crate::Permission>,
156 #[cfg(feature = "live")]
157 pub(crate) live: Vec<String>,
158 #[cfg(feature = "screen-cache")]
159 pub(crate) cached: Option<std::time::Duration>,
160 #[cfg(feature = "jobs")]
162 pub(crate) queues: bool,
163}
164
165pub struct Routes<A: Area> {
167 pub(crate) module: &'static str,
168 pub(crate) added: Vec<Route>,
169 #[cfg(feature = "islands")]
170 pub(crate) islands: Vec<crate::islands::IslandScreen>,
171 area: PhantomData<fn() -> A>,
172}
173
174pub struct PageRoute<'r, A: Area> {
175 route: &'r mut Route,
176 area: PhantomData<fn() -> A>,
177}
178
179pub struct ActionRoute<'r, A: Area> {
180 route: &'r mut Route,
181 area: PhantomData<fn() -> A>,
182}
183
184macro_rules! modifiers {
185 ($($builder:ident),*) => {$(
186 impl<'r, A: Area> $builder<'r, A> {
187 pub(crate) fn new(route: &'r mut Route) -> Self {
188 Self {
189 route,
190 area: PhantomData,
191 }
192 }
193
194 pub fn name(&mut self, handler: &str) -> &mut Self {
196 self.route.name_as(A::NAME, handler);
197 self
198 }
199
200 pub fn name_for<Member: Area>(&mut self, handler: &str) -> &mut Self {
203 self.route.name_as(Member::NAME, handler);
204 self
205 }
206
207 pub fn throttle(&mut self, rate: Rate) -> &mut Self {
209 self.rule(|rules| rules.throttle = Some(rate))
210 }
211
212 #[cfg(feature = "signed-links")]
213 pub fn signed(&mut self) -> &mut Self {
214 self.rule(|rules| rules.signed = true)
215 }
216
217 #[cfg(feature = "flags")]
219 pub fn feature(&mut self, feature: sofie_flags::Feature) -> &mut Self {
220 self.rule(|rules| rules.feature = Some(feature))
221 }
222
223 #[cfg(feature = "entitlements")]
225 pub fn entitled(&mut self, entitlement: crate::Entitlement) -> &mut Self {
226 self.rule(|rules| rules.entitlement = Some(entitlement))
227 }
228
229 pub(crate) fn rule(&mut self, change: impl FnOnce(&mut RouteRules)) -> &mut Self {
230 change(&mut self.route.rules);
231 self
232 }
233 }
234 )*};
235}
236
237modifiers!(PageRoute, ActionRoute);
238
239macro_rules! actions {
240 ($($name:ident: $method:ident),*) => {$(
241 pub fn $name<Args: 'static, H>(&mut self, path: &str, handler: H) -> ActionRoute<'_, A>
242 where
243 H: Call<RequestContext<A>, Args, Outcome: Send>,
244 {
245 let endpoint = endpoint(handler);
246 ActionRoute::new(self.push(Method::$method, path, endpoint, View::Json, RouteRules::default()))
247 }
248 )*};
249}
250
251impl<A: Area> Routes<A> {
252 pub(crate) fn new(module: &'static str) -> Self {
253 Self {
254 module,
255 added: Vec::new(),
256 #[cfg(feature = "islands")]
257 islands: Vec::new(),
258 area: PhantomData,
259 }
260 }
261
262 actions!(post: POST, put: PUT, patch: PATCH, delete: DELETE);
263
264 pub fn get<Args: 'static, H: Loader<A, Args>>(
266 &mut self,
267 path: &str,
268 loader: H,
269 ) -> PageRoute<'_, A> {
270 let endpoint = loader_endpoint(loader);
271 PageRoute::new(self.push(
272 Method::GET,
273 path,
274 endpoint,
275 View::Json,
276 RouteRules::default(),
277 ))
278 }
279
280 #[cfg(feature = "engine")]
281 pub fn screen<Args: 'static, H: Loader<A, Args>>(
282 &mut self,
283 path: &str,
284 name: &str,
285 loader: H,
286 ) -> PageRoute<'_, A> {
287 let view = View::Screen(ScreenView {
288 key: screen_key(self.module, A::NAME, name),
289 title: sofie_engine::humanize(name),
290 #[cfg(feature = "resources")]
291 builtin: None,
292 });
293 let endpoint = loader_endpoint(loader);
294 PageRoute::new(self.push(Method::GET, path, endpoint, view, RouteRules::default()))
295 }
296
297 pub(crate) fn push(
298 &mut self,
299 method: Method,
300 path: &str,
301 endpoint: Endpoint,
302 view: View,
303 rules: RouteRules,
304 ) -> &mut Route {
305 self.added.push_mut(Route {
306 module: self.module,
307 name: None,
308 method,
309 path: format!("{}{path}", A::PREFIX),
310 audience: Audience::of::<A>(),
311 endpoint,
312 view,
313 rules,
314 })
315 }
316}
317
318impl<A: Area> PageRoute<'_, A> {
319 pub fn document(&mut self, media: Media) -> &mut Self {
321 self.route.view = View::Document(media);
322 self
323 }
324}
325
326impl<A: Area> ActionRoute<'_, A> {
327 pub fn body_limit(&mut self, bytes: usize) -> &mut Self {
328 self.rule(|rules| rules.body_limit = Some(bytes))
329 }
330
331 #[cfg(feature = "uploads")]
333 pub fn file_limit(&mut self, bytes: usize) -> &mut Self {
334 self.rule(|rules| rules.file_limit = Some(bytes))
335 }
336
337 #[cfg(feature = "idempotency")]
339 pub fn idempotent(&mut self) -> &mut Self {
340 self.rule(|rules| rules.idempotent = true)
341 }
342
343 #[cfg(feature = "engine")]
345 pub fn then_screen(&mut self, name: &str, fragment: &str) {
346 let refresh = Refresh {
347 screen: screen_key(self.route.module, A::NAME, name),
348 fragment: fragment.to_owned(),
349 };
350 self.route.view = View::Refresh(refresh);
351 }
352
353 pub fn then_redirect(&mut self, location: &str) {
357 self.route.view = View::Redirect(location.to_owned());
358 }
359
360 pub fn then_follow(&mut self) {
362 self.route.view = View::Follow;
363 }
364
365 pub fn then_redirect_or_follow(&mut self, location: &str) {
368 self.route.view = View::RedirectOrFollow(location.to_owned());
369 }
370}