renox_core/registry.rs
1use std::any::TypeId;
2use std::collections::HashMap;
3use std::future::Future;
4
5use crate::events::{Event, listener};
6use crate::queue::{Job, JobHandler, handler};
7use crate::schedule::Schedule;
8use crate::{AppState, Result};
9
10/// Where the app and its modules register jobs, listeners, scheduled tasks
11/// and commands. Modules get it in `Module::register`.
12#[derive(Default)]
13pub struct Registry {
14 pub(crate) jobs: HashMap<&'static str, JobHandler>,
15 pub(crate) listeners: HashMap<TypeId, Vec<crate::events::ListenerFn>>,
16 pub(crate) schedule: Schedule,
17 pub(crate) duplicate_job: Option<&'static str>,
18 pub(crate) webhooks: HashMap<&'static str, crate::webhook::HandleFn>,
19 pub(crate) commands: Vec<crate::command::Command>,
20 pub(crate) templates: Vec<crate::view::TemplateHook>,
21 pub(crate) shares: Vec<(String, crate::view::ShareFn)>,
22 pub(crate) channels: HashMap<String, crate::auth::notifications::ChannelFn>,
23 pub(crate) reporters: Vec<crate::report::ReportFn>,
24 /// The `Permissions` module is on: load each user's roles.
25 pub(crate) permissions: bool,
26 /// A second login step (`second_factor`), and whether two modules set one.
27 pub(crate) second_factor: Option<crate::auth::second_factor::SecondFactor>,
28 pub(crate) duplicate_second_factor: bool,
29 /// Sections other modules add to the `/account` page.
30 pub(crate) account_sections: Vec<crate::auth::account::AccountSection>,
31 /// The `Auth` module's settings, for other ways of logging in
32 /// (`auth::sign_in`, `auth::register_verified`).
33 pub(crate) auth: Option<std::sync::Arc<crate::auth::module::Settings>>,
34 /// Files modules serve as they are (`asset`): path, content type, body.
35 pub(crate) assets: Vec<StaticAsset>,
36 /// Values modules provide (`provide`), under the app's own `App::provide`.
37 pub(crate) provided: HashMap<TypeId, std::sync::Arc<dyn std::any::Any + Send + Sync>>,
38}
39
40/// A file a module serves as it is (`Registry::asset`).
41#[derive(Clone, Copy)]
42pub(crate) struct StaticAsset {
43 pub(crate) path: &'static str,
44 pub(crate) content_type: &'static str,
45 pub(crate) body: &'static [u8],
46}
47
48impl Registry {
49 /// Lets workers run jobs of type `J`.
50 pub fn job<J: Job>(&mut self) -> &mut Self {
51 if self.jobs.insert(J::NAME, handler::<J>()).is_some() {
52 self.duplicate_job.get_or_insert(J::NAME);
53 }
54 self
55 }
56
57 /// Lets queue workers process `W`'s webhooks; pair it with
58 /// `Routes::webhook::<W>(path)`.
59 pub fn webhook<W: crate::webhook::Webhook>(&mut self) -> &mut Self {
60 if self
61 .webhooks
62 .insert(W::PROVIDER, crate::webhook::handler::<W>())
63 .is_some()
64 {
65 self.duplicate_job.get_or_insert(W::PROVIDER);
66 }
67 self
68 }
69
70 /// Runs `listener` whenever an `E` is emitted.
71 pub fn listen<E, F, Fut>(&mut self, listener_fn: F) -> &mut Self
72 where
73 E: Event,
74 F: Fn(E, AppState) -> Fut + Send + Sync + 'static,
75 Fut: Future<Output = Result> + Send + 'static,
76 {
77 let (type_id, run) = listener(listener_fn);
78 self.listeners.entry(type_id).or_default().push(run);
79 self
80 }
81
82 /// Adds a command the app binary runs: `my-app <name> [args]`. See
83 /// [`crate::command`].
84 pub fn command<F, Fut>(&mut self, name: &str, about: &str, run: F) -> &mut Self
85 where
86 F: Fn(crate::command::Args, AppState) -> Fut + Send + Sync + 'static,
87 Fut: Future<Output = Result> + Send + 'static,
88 {
89 self.commands
90 .push(crate::command::command(name, about, run));
91 self
92 }
93
94 /// A command whose arguments are declared with clap; see
95 /// [`AppCommand`](crate::command::AppCommand).
96 pub fn typed_command<T: crate::command::AppCommand>(&mut self) -> &mut Self {
97 self.commands.push(crate::command::typed::<T>());
98 self
99 }
100
101 /// Adds template functions, filters or globals, e.g. a `euros` filter
102 /// that always writes cents the German way:
103 ///
104 /// ```
105 /// # use renox::prelude::*;
106 /// # let _ =
107 /// App::new().templates(|env| {
108 /// env.add_filter("euros", |cents: i64| format!("{} €", renox::format_number(cents as f64 / 100.0, 2, "de")));
109 /// })
110 /// # ;
111 /// ```
112 ///
113 /// Built in: `number` (`{{ price | number }}` → `75.000` in German,
114 /// `number(2)` for decimals) and `date` (`{{ created_at | date("%d/%m/%Y") }}`,
115 /// in `APP_TIMEZONE`).
116 pub fn templates(
117 &mut self,
118 hook: impl Fn(&mut minijinja::Environment<'static>) + Send + Sync + 'static,
119 ) -> &mut Self {
120 self.templates.push(std::sync::Arc::new(hook));
121 self
122 }
123
124 /// Serves `body` at `path` the way Renox serves its own scripts and
125 /// styles: in front of sessions, CSRF and maintenance mode (no cookie is
126 /// set), with a year-long `immutable` cache. For a module's JavaScript,
127 /// CSS or fonts, compiled into its crate; put a version or a hash of the
128 /// content in `path`, so a new release gets a new address.
129 ///
130 /// ```
131 /// # use renox::prelude::*;
132 /// struct Charts;
133 ///
134 /// impl Module for Charts {
135 /// fn name(&self) -> &'static str { "charts" }
136 ///
137 /// fn register(&self, app: &mut Registry) {
138 /// app.asset(
139 /// "/_charts/charts-1.2.0.js",
140 /// "text/javascript; charset=utf-8",
141 /// b"console.log('charts')",
142 /// );
143 /// }
144 /// }
145 /// ```
146 ///
147 /// The path must start with `/`; two files at the same path stop the
148 /// app at boot.
149 pub fn asset(
150 &mut self,
151 path: &'static str,
152 content_type: &'static str,
153 body: &'static [u8],
154 ) -> &mut Self {
155 self.assets.push(StaticAsset {
156 path,
157 content_type,
158 body,
159 });
160 self
161 }
162
163 /// Gives every view `key`, computed per request, e.g. the categories in
164 /// a menu or the number of items in a cart. It runs for every rendered
165 /// view, so keep it quick (cache what doesn't change per request).
166 ///
167 /// ```
168 /// # use renox::prelude::*;
169 /// # use std::time::Duration;
170 /// # let _ =
171 /// App::new().share("cart_count", |ctx: renox::view::ViewContext| async move {
172 /// let Some(user) = ctx.user else { return Ok(0) };
173 /// let n: i64 = renox::db::sql("SELECT COUNT(*) FROM cart_items WHERE user_id = ?")
174 /// .bind(user.id)
175 /// .scalar(&ctx.state.db)
176 /// .await?;
177 /// Ok(n)
178 /// })
179 /// # ;
180 /// ```
181 ///
182 /// Values a handler passes in `context!` win over shared ones.
183 pub fn share<F, Fut, T>(&mut self, key: &str, compute: F) -> &mut Self
184 where
185 F: Fn(crate::view::ViewContext) -> Fut + Send + Sync + 'static,
186 Fut: Future<Output = Result<T>> + Send + 'static,
187 T: serde::Serialize,
188 {
189 self.shares
190 .push((key.to_owned(), crate::view::share_fn(compute)));
191 self
192 }
193
194 /// Adds a notification channel, used by notifications that list
195 /// `Channel::Custom(name)`: `send` gets the recipient and the message
196 /// `Notification::to_channel` built. See [`crate::auth::notifications`].
197 pub fn channel<F, Fut>(&mut self, name: &str, send: F) -> &mut Self
198 where
199 F: Fn(crate::auth::Recipient, serde_json::Value, AppState) -> Fut + Send + Sync + 'static,
200 Fut: Future<Output = Result> + Send + 'static,
201 {
202 self.channels.insert(
203 name.to_owned(),
204 crate::auth::notifications::channel_fn(send),
205 );
206 self
207 }
208
209 /// Adds a second step to logging in, such as two-factor authentication;
210 /// see [`crate::auth::second_factor`]. After the right password, a user
211 /// for whom `required` answers `true` isn't logged in yet: the browser
212 /// goes to the route named `challenge`, whose handler checks the code and
213 /// calls [`crate::auth::complete_login`]. One module may set it; a second
214 /// one is an error at boot.
215 pub fn second_factor<F, Fut>(&mut self, challenge: &str, required: F) -> &mut Self
216 where
217 F: Fn(crate::auth::User, AppState) -> Fut + Send + Sync + 'static,
218 Fut: Future<Output = Result<bool>> + Send + 'static,
219 {
220 if self.second_factor.is_some() {
221 self.duplicate_second_factor = true;
222 }
223 self.second_factor = Some(crate::auth::second_factor::second_factor(
224 challenge, required,
225 ));
226 self
227 }
228
229 /// Adds a section to the `Auth` module's `/account` page (with
230 /// `Auth::new().account()`), such as two-factor authentication or linked
231 /// logins. `template` is rendered with the page's context; `data` runs
232 /// for the logged-in user on every visit, and the template reads what it
233 /// returns as `section.data`. Sections show in `order` (then in the order
234 /// they were added), after the built-in cards and before "Delete account".
235 ///
236 /// ```
237 /// # use renox::prelude::*;
238 /// struct Pin;
239 ///
240 /// impl Module for Pin {
241 /// fn name(&self) -> &'static str {
242 /// "pin"
243 /// }
244 ///
245 /// fn register(&self, app: &mut Registry) {
246 /// app.templates(|env| {
247 /// env.add_template(
248 /// "pin/account.html",
249 /// r#"<section id="pin">PIN {{ "on" if section.data.on else "off" }}</section>"#,
250 /// )
251 /// .unwrap();
252 /// });
253 /// app.account_section("pin/account.html", 10, |user, _state| async move {
254 /// Ok(json!({ "on": user.extra.contains_key("pin") }))
255 /// });
256 /// }
257 /// }
258 /// ```
259 pub fn account_section<F, Fut>(&mut self, template: &str, order: i32, data: F) -> &mut Self
260 where
261 F: Fn(crate::auth::User, AppState) -> Fut + Send + Sync + 'static,
262 Fut: Future<Output = Result<serde_json::Value>> + Send + 'static,
263 {
264 self.account_sections
265 .push(crate::auth::account::section(template, order, data));
266 self
267 }
268
269 /// Sends every error that needs a person to `reporter`; see
270 /// [`crate::report`].
271 pub fn report<F, Fut>(&mut self, reporter: F) -> &mut Self
272 where
273 F: Fn(crate::report::ErrorReport, AppState) -> Fut + Send + Sync + 'static,
274 Fut: Future<Output = ()> + Send + 'static,
275 {
276 self.reporters.push(crate::report::report_fn(reporter));
277 self
278 }
279
280 /// Makes `value` available everywhere the app runs, as
281 /// [`App::provide`](crate::App::provide) does: `Provided<T>` in handlers,
282 /// `state.provided::<T>()` in jobs, listeners, webhooks and commands. For
283 /// a module's settings that code without a request needs (a payment
284 /// module's plans in its webhook handler). One value per type; a value
285 /// the app gives with `App::provide` wins.
286 ///
287 /// ```
288 /// # use renox::prelude::*;
289 /// struct Plans(Vec<&'static str>);
290 ///
291 /// struct Billing;
292 ///
293 /// impl Module for Billing {
294 /// fn name(&self) -> &'static str {
295 /// "billing"
296 /// }
297 ///
298 /// fn register(&self, app: &mut Registry) {
299 /// app.provide(Plans(vec!["basic", "pro"]));
300 /// }
301 /// }
302 ///
303 /// async fn in_a_job(state: AppState) {
304 /// let plans = state.provided::<Plans>().expect("the Billing module is on");
305 /// # let _ = plans;
306 /// }
307 /// ```
308 pub fn provide<T: Send + Sync + 'static>(&mut self, value: T) -> &mut Self {
309 self.provided
310 .insert(TypeId::of::<T>(), std::sync::Arc::new(value));
311 self
312 }
313
314 /// The schedule, to add tasks to.
315 pub fn schedule(&mut self) -> &mut Schedule {
316 &mut self.schedule
317 }
318}