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}