inertia_rust/inertia.rs
1use crate::config::InertiaConfig;
2use crate::node_process::NodeJsProc;
3use crate::props::InertiaProps;
4use crate::req_type::InertiaRequestType;
5use crate::template_resolver::TemplateResolver;
6use crate::{InertiaError, InertiaPage, InertiaSSRPage};
7use async_trait::async_trait;
8use reqwest::Url;
9use serde::{Deserialize, Serialize};
10use serde_json::{Map, Value};
11use std::collections::HashMap;
12use std::io;
13
14pub const X_INERTIA: &str = "x-inertia";
15pub const X_INERTIA_LOCATION: &str = "x-inertia-location";
16pub const X_INERTIA_VERSION: &str = "x-inertia-version";
17pub const X_INERTIA_PARTIAL_COMPONENT: &str = "x-inertia-partial-component";
18pub const X_INERTIA_PARTIAL_DATA: &str = "x-inertia-partial-data";
19pub const X_INERTIA_PARTIAL_EXCEPT: &str = "x-inertia-partial-except";
20pub const X_INERTIA_RESET: &str = "x-inertia-reset";
21pub const X_INERTIA_ERROR_BAG: &str = "x-inertia-error-bag";
22
23/// The javascript component name.
24#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
25pub struct Component(pub String);
26
27impl<T> From<T> for Component
28where
29 T: ToString,
30{
31 fn from(value: T) -> Self {
32 Component(value.to_string())
33 }
34}
35
36/// InertiaService trait define a method to be implemented to Inertia struct that allows
37/// to generate simple routes directly, without needing to create a handler function.
38pub trait InertiaService {
39 /// Renders an Inertia component directly, without defining a specific handler function for it.
40 ///
41 /// # Arguments
42 /// * `path` - The router path.
43 /// * `component` - The component to be rendered.
44 ///
45 /// # Examples
46 /// ```ignore
47 /// use some_framework::App;
48 ///
49 /// App::new().inertia_route("/", "Index");
50 /// ```
51 fn inertia_route(self, path: &str, component: &'static str) -> Self;
52}
53
54/// InertiaResponder trait defines methods that every provider
55/// must implement. For instance, T may be a sort of actix-web Responder,
56/// if "actix" feature is passed with the --feature flag or with the
57/// feature field in the cargo toml.
58#[async_trait(?Send)] // it's `?Send` because some frameworks like Actix won't require requests to be thread-safe
59pub trait InertiaResponder<TResponder, THttpRequest, TRedirect> {
60 async fn inner_render<'b>(
61 &'b self,
62 req: &'b THttpRequest,
63 component: Component,
64 props: Option<InertiaProps<'b>>,
65 ) -> Result<TResponder, InertiaError>;
66
67 fn inner_back(&self, req: &THttpRequest) -> TRedirect;
68
69 fn inner_back_with_errors<Key: ToString>(
70 &self,
71 req: &THttpRequest,
72 errors: HashMap<Key, Value>,
73 ) -> TRedirect;
74
75 fn inner_location(req: &THttpRequest, url: &str) -> TResponder;
76
77 fn inner_encrypt_history(req: &THttpRequest, encrypt: bool);
78
79 fn inner_clear_history(req: &THttpRequest);
80}
81
82/// Defines some helper methods to be implemented to HttpRequests from the
83/// library opted by the cargo feature.
84pub(crate) trait InertiaHttpRequest {
85 fn should_clear_history(&self) -> bool;
86
87 fn should_encrypt_history(&self, default: bool) -> bool;
88
89 fn get_merge_props_to_be_reset(&self) -> Vec<&str>;
90
91 fn is_inertia_request(&self) -> bool;
92
93 fn get_request_type(&self) -> Result<InertiaRequestType, InertiaError>;
94
95 /// Checks if application assets version matches.
96 /// If the request contains the inertia version header, it will be checked.
97 /// Otherwise, it means it does not have outdated assets and can also pass.
98 fn check_inertia_version(&self, current_version: &str) -> bool;
99}
100
101pub enum InertiaVersion<T>
102where
103 T: ToString,
104{
105 Literal(T),
106 Resolver(Box<dyn FnOnce() -> T>),
107}
108
109impl<T> InertiaVersion<T>
110where
111 T: ToString,
112{
113 pub fn resolve(self) -> &'static str {
114 match self {
115 InertiaVersion::Literal(v) => v.to_string().leak(),
116 InertiaVersion::Resolver(resolver) => resolver().to_string().leak(),
117 }
118 }
119}
120
121/// View Data is a struct containing props to be used by the root template.
122pub struct ViewData<'a> {
123 pub page: InertiaPage<'a>,
124 pub ssr_page: Option<InertiaSSRPage>,
125 pub custom_props: Map<String, Value>,
126}
127
128#[derive(PartialEq, Debug)]
129pub struct SsrClient {
130 pub(crate) host: &'static str,
131 pub(crate) port: u16,
132}
133
134impl SsrClient {
135 /// Generates a new custom `SsrClient` struct. Unless you really need to change the ssr server
136 /// url, it is preferred to use `SsrClient::Default` for generating a new SsrClient struct.
137 ///
138 /// # Arguments
139 /// * `host` - The host of the server (normally, `127.0.0.1`, since it should run locally
140 /// * `port` - The server port
141 pub fn new(host: &'static str, port: u16) -> Self {
142 Self { host, port }
143 }
144}
145
146impl Default for SsrClient {
147 fn default() -> Self {
148 Self {
149 host: "127.0.0.1",
150 port: 13714,
151 }
152 }
153}
154
155/// Inertia struct must be a singleton and initialized at the application bootstrap.
156/// It is supposed to last during the whole application runtime.
157///
158/// Extra details of how to initialize and keep it is specific to the feature-opted http library.
159pub struct Inertia {
160 /// URL used between redirects and responses generation, i.g. "https://myapp.com".
161 #[allow(unused)]
162 pub(crate) url: &'static str,
163 /// The current assets version.
164 pub(crate) version: &'static str,
165 /// A struct that implements [TemplateResolver] trait.
166 pub(crate) template_resolver: Box<dyn TemplateResolver + Send + Sync>,
167 /// Address of Inertia local render server. Will be used by Inertia to perform ssr.
168 pub(crate) ssr_url: Option<Url>,
169 /// Whether to encrypt or not the page data stored in the browser history.
170 pub(crate) encrypt_history: bool,
171}
172
173impl Inertia {
174 /// Initializes an instance of [`Inertia`] struct.
175 ///
176 /// # Arguments
177 /// * `config` - A [`InertiaConfig`] instance.
178 ///
179 /// # Errors
180 /// Returns an [`InertiaError::SsrError`] if it fails to connect to the server.
181 pub fn new<V>(config: InertiaConfig<V>) -> Result<Self, io::Error>
182 where
183 V: ToString,
184 {
185 let version = config.version.resolve();
186 let ssr_url = match config.with_ssr {
187 false => None,
188 true => {
189 let client: SsrClient = config.custom_ssr_client.unwrap_or_default();
190
191 let ssr_url = if client.host.contains("://") {
192 format!("{}:{}", client.host, client.port)
193 } else {
194 format!("http://{}:{}", client.host, client.port)
195 };
196
197 match Url::parse(&ssr_url) {
198 Err(err) => {
199 let inertia_err = InertiaError::SsrError(format!(
200 "Failed to parse Inertia Server url: {}",
201 err
202 ));
203 return Err(inertia_err.to_io_error());
204 }
205 Ok(url) => Some(url),
206 }
207 }
208 };
209
210 Ok(Self {
211 url: config.url,
212 version,
213 template_resolver: config.template_resolver,
214 ssr_url,
215 encrypt_history: config.encrypt_history,
216 })
217 }
218
219 /// Instantiates a [`NodeJsProc`] by calling [`NodeJsProc::start`] with the given path and the
220 /// inertia `ssr_url` as server url.
221 ///
222 /// # Arguments
223 /// * `server_file_path` - The path to the server javascript file. E.g. "dist/server/ssr.js".
224 ///
225 /// # Errors
226 /// Will return an [`InertiaError`] if ssr is not enabled or if something goes wrong on setting
227 /// the node.js server up (if your machine do not have node installed, for example).
228 ///
229 /// # Return
230 /// Returns a [`NodeJsProc`] instance.
231 ///
232 /// # Example
233 /// ```rust
234 /// use inertia_rust::node_process::NodeJsProc;
235 /// use inertia_rust::{
236 /// template_resolvers::TemplateResolver,
237 /// Inertia,
238 /// InertiaVersion,
239 /// InertiaError,
240 /// ViewData,
241 /// InertiaConfig
242 /// };
243 /// use std::pin::Pin;
244 /// use std::future::Future;
245 ///
246 /// async fn server() {
247 /// struct MyTemplateResolver;
248 ///
249 /// #[async_trait::async_trait(?Send)]
250 /// impl TemplateResolver for MyTemplateResolver {
251 /// async fn resolve_template(
252 /// &self,
253 /// view_data: ViewData<'_>,
254 /// ) -> Result<String, InertiaError> {
255 /// // import the layout root and render it using your template engine
256 /// // lets pretend we rendered it, so it ended up being the html output below!
257 /// Ok("<h1>my rendered page!</h1>".to_string())
258 /// }
259 /// }
260 ///
261 /// let inertia = Inertia::new(
262 /// InertiaConfig::builder()
263 /// .set_url("https://www.my-web-app.com")
264 /// .set_version(InertiaVersion::Literal("my-assets-version"))
265 /// .set_template_resolver(Box::new(MyTemplateResolver))
266 /// .build()
267 /// )
268 /// .unwrap();
269 ///
270 /// let node: Result<NodeJsProc, std::io::Error> = inertia.start_node_server("dist/server/ssr.js".into());
271 /// if node.is_err() {
272 /// let err = node.unwrap_err();
273 /// panic!("Failed to start inertia ssr server: {:?}", err);
274 /// }
275 ///
276 /// let node = node.unwrap();
277 ///
278 /// // starts your server here, using inertia.
279 /// // httpserver().await; or something like this
280 ///
281 /// let _ = node.kill(); // don't forget to kill the node.js process on shutdown
282 /// }
283 /// ```
284 pub fn start_node_server(&self, server_file_path: String) -> Result<NodeJsProc, io::Error> {
285 if self.ssr_url.is_none() {
286 let inertia_err: InertiaError = InertiaError::SsrError(
287 "Ssr is not enabled and, hence, a ssr server cannot be raised.".into(),
288 );
289 return Err(inertia_err.to_io_error());
290 }
291
292 let node = NodeJsProc::start(server_file_path, self.ssr_url.as_ref().unwrap());
293 match node {
294 Err(err) => Err(InertiaError::NodeJsError(err).to_io_error()),
295 Ok(process) => Ok(process),
296 }
297 }
298
299 pub fn get_url(&self) -> &'static str {
300 self.url
301 }
302
303 pub fn get_version(&self) -> &'static str {
304 self.version
305 }
306
307 pub fn get_ssr_url(&self) -> &Option<Url> {
308 &self.ssr_url
309 }
310}