Skip to main content

renox_core/live_component/
context.rs

1//! What an action can reach: the request, and what it sends back.
2
3use axum::extract::{FromRequestParts, OptionalFromRequestParts};
4use axum::http::request::Parts;
5use serde::Serialize;
6
7use crate::validation::{ValidationError, Validator};
8use crate::{AppState, AuthUser, Error, Result, Session, Toast, Validate};
9
10/// The request behind a live component's action, and the effects it asks for
11/// (a toast, a redirect, events).
12///
13/// ```
14/// # use renox::prelude::*;
15/// # use renox::live_component::LiveContext;
16/// # fn demo(ctx: &mut LiveContext) -> Result {
17/// ctx.toast(Toast::success("Saved"));
18/// ctx.dispatch("saved", json!({ "id": 3 }));
19/// ctx.redirect("/done");
20/// # Ok(()) }
21/// ```
22pub struct LiveContext {
23    pub(crate) state: AppState,
24    session: Option<Session>,
25    user: Option<AuthUser>,
26    pub(crate) component: &'static str,
27    pub(crate) toast: Option<Toast>,
28    pub(crate) redirect: Option<String>,
29    pub(crate) events: Vec<(String, serde_json::Value)>,
30}
31
32impl LiveContext {
33    /// The application state.
34    ///
35    /// ```
36    /// # use renox::prelude::*;
37    /// # use renox::live_component::LiveContext;
38    /// # fn demo(ctx: &LiveContext) { let _db = &ctx.state().db; }
39    /// ```
40    pub fn state(&self) -> &AppState {
41        &self.state
42    }
43
44    /// Renders `component` for a page: its state, its data and a signed
45    /// snapshot. Pass the result to the page's template and
46    /// `{% include "renox/live.html" %}` it as `component`.
47    ///
48    /// ```
49    /// # use renox::prelude::*;
50    /// # use renox::live_component::LiveContext;
51    /// # #[derive(serde::Serialize, serde::Deserialize)]
52    /// # struct Counter { count: i64 }
53    /// # impl LiveComponent for Counter {
54    /// #     const NAME: &'static str = "counter";
55    /// #     const VIEW: &'static str = "live/counter.html";
56    /// #     async fn call(&mut self, _: &str, _: Vec<serde_json::Value>, _: &mut LiveContext) -> Result { Ok(()) }
57    /// # }
58    /// # async fn demo(ctx: LiveContext) -> Result {
59    /// let mounted = ctx.mount(Counter { count: 0 }).await?;
60    /// # let _ = mounted; Ok(()) }
61    /// ```
62    pub async fn mount<C: super::LiveComponent>(&self, c: C) -> Result<super::Mounted> {
63        let id = format!("rx-live-{}", &crate::random_token()[..12]);
64        super::render(self, &c, id, false).await
65    }
66
67    /// The request's session, when there is one.
68    ///
69    /// ```
70    /// # use renox::prelude::*;
71    /// # use renox::live_component::LiveContext;
72    /// # fn demo(ctx: &LiveContext) { let _ = ctx.session(); }
73    /// ```
74    pub fn session(&self) -> Option<&Session> {
75        self.session.as_ref()
76    }
77
78    /// The signed-in user; `Error::Unauthorized` when nobody is.
79    ///
80    /// ```
81    /// # use renox::prelude::*;
82    /// # use renox::live_component::LiveContext;
83    /// # fn demo(ctx: &LiveContext) -> Result { let _user = ctx.user()?; Ok(()) }
84    /// ```
85    pub fn user(&self) -> Result<&AuthUser> {
86        self.user.as_ref().ok_or(Error::Unauthorized)
87    }
88
89    /// Sends a toast with the answer.
90    ///
91    /// ```
92    /// # use renox::prelude::*;
93    /// # use renox::live_component::LiveContext;
94    /// # fn demo(ctx: &mut LiveContext) { ctx.toast(Toast::success("Saved")); }
95    /// ```
96    pub fn toast(&mut self, toast: Toast) {
97        self.toast = Some(toast);
98    }
99
100    /// Sends the browser to `url` (an `HX-Redirect`).
101    ///
102    /// ```
103    /// # use renox::prelude::*;
104    /// # use renox::live_component::LiveContext;
105    /// # fn demo(ctx: &mut LiveContext) { ctx.redirect("/orders"); }
106    /// ```
107    pub fn redirect(&mut self, url: impl Into<String>) {
108        self.redirect = Some(url.into());
109    }
110
111    /// Fires the event `rx:<component>:<name>` on the component, with `detail`.
112    ///
113    /// ```
114    /// # use renox::prelude::*;
115    /// # use renox::live_component::LiveContext;
116    /// # fn demo(ctx: &mut LiveContext) { ctx.dispatch("saved", json!({ "id": 1 })); }
117    /// ```
118    pub fn dispatch(&mut self, name: &str, detail: serde_json::Value) {
119        self.events
120            .push((format!("rx:{}:{name}", self.component), detail));
121    }
122
123    /// Checks `value` against its rules; a failure is a 422 with the field
124    /// errors (and the input, for the form to show again).
125    ///
126    /// ```
127    /// # use renox::prelude::*;
128    /// # use renox::live_component::LiveContext;
129    /// # #[derive(serde::Serialize)]
130    /// # struct Form { name: String }
131    /// # impl Validate for Form {
132    /// #     fn rules(&self, v: &mut Validator) { v.field("name", &self.name).required(); }
133    /// # }
134    /// # async fn demo(ctx: &LiveContext, form: &Form) -> Result {
135    /// ctx.validate(form).await?;
136    /// # Ok(()) }
137    /// ```
138    pub async fn validate(&self, value: &(impl Validate + Serialize)) -> Result {
139        let errors = Validator::rules_of(value)
140            .finish_for(&self.state, self.user.as_ref().map(|u| u.user()))
141            .await?;
142        if errors.is_empty() {
143            Ok(())
144        } else {
145            Err(ValidationError::new(errors).with_input(value).into())
146        }
147    }
148}
149
150impl<S: Send + Sync> FromRequestParts<S> for LiveContext {
151    type Rejection = Error;
152
153    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self> {
154        let app = parts
155            .extensions
156            .get::<AppState>()
157            .cloned()
158            .ok_or_else(|| anyhow::anyhow!("the auth middleware is not installed"))?;
159        let session = parts.extensions.get::<Session>().cloned();
160        let user = match <AuthUser as OptionalFromRequestParts<S>>::from_request_parts(parts, state)
161            .await
162        {
163            Ok(user) => user,
164            Err(never) => match never {},
165        };
166        Ok(Self {
167            state: app,
168            session,
169            user,
170            component: "",
171            toast: None,
172            redirect: None,
173            events: Vec::new(),
174        })
175    }
176}