Skip to main content

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}