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}