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