1use crate::config::InertiaConfig;
4use crate::request::InertiaRequest;
5use crate::shared::InertiaShared;
6use serde::Serialize;
7
8#[derive(Debug, Clone)]
12pub struct InertiaHttpResponse {
13 pub status: u16,
15 pub headers: Vec<(String, String)>,
17 pub body: String,
19 pub content_type: &'static str,
21}
22
23impl InertiaHttpResponse {
24 pub fn json(body: impl Into<String>) -> Self {
26 Self {
27 status: 200,
28 headers: vec![
29 ("X-Inertia".to_string(), "true".to_string()),
30 ("Vary".to_string(), "X-Inertia".to_string()),
31 ],
32 body: body.into(),
33 content_type: "application/json",
34 }
35 }
36
37 pub fn raw_json(body: impl Into<String>) -> Self {
41 Self {
42 status: 200,
43 headers: vec![],
44 body: body.into(),
45 content_type: "application/json",
46 }
47 }
48
49 pub fn html(body: impl Into<String>) -> Self {
51 Self {
52 status: 200,
53 headers: vec![("Vary".to_string(), "X-Inertia".to_string())],
54 body: body.into(),
55 content_type: "text/html; charset=utf-8",
56 }
57 }
58
59 pub fn conflict(location: impl Into<String>) -> Self {
61 Self {
62 status: 409,
63 headers: vec![("X-Inertia-Location".to_string(), location.into())],
64 body: String::new(),
65 content_type: "text/plain",
66 }
67 }
68
69 pub fn status(mut self, status: u16) -> Self {
71 self.status = status;
72 self
73 }
74
75 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
77 self.headers.push((name.into(), value.into()));
78 self
79 }
80
81 pub fn redirect(location: impl Into<String>, is_post_like: bool) -> Self {
88 let status = if is_post_like { 303 } else { 302 };
91
92 Self {
93 status,
94 headers: vec![
95 ("X-Inertia".to_string(), "true".to_string()),
96 ("Location".to_string(), location.into()),
97 ],
98 body: String::new(),
99 content_type: "text/plain",
100 }
101 }
102}
103
104pub struct Inertia;
108
109impl Inertia {
110 pub fn render<R, P>(req: &R, component: &str, props: P) -> InertiaHttpResponse
129 where
130 R: InertiaRequest,
131 P: Serialize,
132 {
133 Self::render_internal(req, component, props, None, InertiaConfig::default(), false)
134 }
135
136 pub fn render_with_json_fallback<R, P>(
159 req: &R,
160 component: &str,
161 props: P,
162 ) -> InertiaHttpResponse
163 where
164 R: InertiaRequest,
165 P: Serialize,
166 {
167 Self::render_internal(req, component, props, None, InertiaConfig::default(), true)
168 }
169
170 pub fn render_with_shared<R, P>(
172 req: &R,
173 component: &str,
174 props: P,
175 shared: &InertiaShared,
176 ) -> InertiaHttpResponse
177 where
178 R: InertiaRequest,
179 P: Serialize,
180 {
181 Self::render_internal(
182 req,
183 component,
184 props,
185 Some(shared),
186 InertiaConfig::default(),
187 false,
188 )
189 }
190
191 pub fn render_with_config<R, P>(
193 req: &R,
194 component: &str,
195 props: P,
196 config: InertiaConfig,
197 ) -> InertiaHttpResponse
198 where
199 R: InertiaRequest,
200 P: Serialize,
201 {
202 Self::render_internal(req, component, props, None, config, false)
203 }
204
205 pub fn render_with_options<R, P>(
207 req: &R,
208 component: &str,
209 props: P,
210 shared: Option<&InertiaShared>,
211 config: InertiaConfig,
212 ) -> InertiaHttpResponse
213 where
214 R: InertiaRequest,
215 P: Serialize,
216 {
217 Self::render_internal(req, component, props, shared, config, false)
218 }
219
220 pub fn render_with_options_and_json_fallback<R, P>(
222 req: &R,
223 component: &str,
224 props: P,
225 shared: Option<&InertiaShared>,
226 config: InertiaConfig,
227 ) -> InertiaHttpResponse
228 where
229 R: InertiaRequest,
230 P: Serialize,
231 {
232 Self::render_internal(req, component, props, shared, config, true)
233 }
234
235 fn render_internal<R, P>(
237 req: &R,
238 component: &str,
239 props: P,
240 shared: Option<&InertiaShared>,
241 config: InertiaConfig,
242 json_fallback: bool,
243 ) -> InertiaHttpResponse
244 where
245 R: InertiaRequest,
246 P: Serialize,
247 {
248 let url = req.path().to_string();
249 let is_inertia = req.is_inertia();
250 let partial_data = req.inertia_partial_data();
251 let partial_component = req.inertia_partial_component();
252
253 let mut props_value = match serde_json::to_value(&props) {
255 Ok(v) => v,
256 Err(e) => {
257 return InertiaHttpResponse::html(format!("Failed to serialize props: {e}"))
258 .status(500);
259 }
260 };
261
262 if let Some(shared) = shared {
264 shared.merge_into(&mut props_value);
265 }
266
267 if is_inertia {
269 if let Some(partial_keys) = partial_data {
270 let should_filter = partial_component.map(|pc| pc == component).unwrap_or(false);
271
272 if should_filter {
273 props_value = Self::filter_partial_props(props_value, &partial_keys);
274 }
275 }
276 }
277
278 if json_fallback && !is_inertia && req.accepts_json() {
282 return InertiaHttpResponse::raw_json(
283 serde_json::to_string(&props_value).unwrap_or_default(),
284 );
285 }
286
287 let response = InertiaResponse::new(component, props_value, url).with_config(config);
288
289 let csrf = shared.and_then(|s| s.csrf.as_deref());
291
292 if is_inertia {
293 response.to_json_response()
294 } else {
295 response.to_html_response(csrf)
296 }
297 }
298
299 pub fn check_version<R: InertiaRequest>(
303 req: &R,
304 current_version: &str,
305 redirect_url: &str,
306 ) -> Option<InertiaHttpResponse> {
307 if !req.is_inertia() {
308 return None;
309 }
310
311 if let Some(client_version) = req.inertia_version() {
312 if client_version != current_version {
313 return Some(InertiaHttpResponse::conflict(redirect_url));
314 }
315 }
316
317 None
318 }
319
320 fn filter_partial_props(props: serde_json::Value, partial_keys: &[&str]) -> serde_json::Value {
322 match props {
323 serde_json::Value::Object(map) => {
324 let filtered: serde_json::Map<String, serde_json::Value> = map
325 .into_iter()
326 .filter(|(k, _)| partial_keys.contains(&k.as_str()))
327 .collect();
328 serde_json::Value::Object(filtered)
329 }
330 other => other,
331 }
332 }
333}
334
335pub struct InertiaResponse {
337 component: String,
338 props: serde_json::Value,
339 url: String,
340 config: InertiaConfig,
341}
342
343impl InertiaResponse {
344 pub fn new(component: impl Into<String>, props: serde_json::Value, url: String) -> Self {
346 Self {
347 component: component.into(),
348 props,
349 url,
350 config: InertiaConfig::default(),
351 }
352 }
353
354 pub fn with_config(mut self, config: InertiaConfig) -> Self {
356 self.config = config;
357 self
358 }
359
360 pub fn to_json_response(&self) -> InertiaHttpResponse {
362 let page = serde_json::json!({
363 "component": self.component,
364 "props": self.props,
365 "url": self.url,
366 "version": self.config.version,
367 });
368
369 InertiaHttpResponse::json(serde_json::to_string(&page).unwrap_or_default())
370 }
371
372 pub fn to_html_response(&self, csrf_token: Option<&str>) -> InertiaHttpResponse {
374 let page_data = serde_json::json!({
375 "component": self.component,
376 "props": self.props,
377 "url": self.url,
378 "version": self.config.version,
379 });
380
381 let page_json = serde_json::to_string(&page_data)
383 .unwrap_or_default()
384 .replace('&', "&")
385 .replace('<', "<")
386 .replace('>', ">")
387 .replace('"', """)
388 .replace('\'', "'");
389
390 let csrf = csrf_token.unwrap_or("");
391
392 if let Some(template) = &self.config.html_template {
394 let html = template
395 .replace("{page}", &page_json)
396 .replace("{csrf}", csrf);
397 return InertiaHttpResponse::html(html);
398 }
399
400 let html = if self.config.development {
402 format!(
403 r#"<!DOCTYPE html>
404<html lang="en">
405<head>
406 <meta charset="UTF-8">
407 <meta name="viewport" content="width=device-width, initial-scale=1.0">
408 <meta name="csrf-token" content="{}">
409 <title>Inertia App</title>
410 <script type="module">
411 import RefreshRuntime from '{}/@react-refresh'
412 RefreshRuntime.injectIntoGlobalHook(window)
413 window.$RefreshReg$ = () => {{}}
414 window.$RefreshSig$ = () => (type) => type
415 window.__vite_plugin_react_preamble_installed__ = true
416 </script>
417 <script type="module" src="{}/@vite/client"></script>
418 <script type="module" src="{}/{}"></script>
419</head>
420<body>
421 <div id="app" data-page="{}"></div>
422</body>
423</html>"#,
424 csrf,
425 self.config.vite_dev_server,
426 self.config.vite_dev_server,
427 self.config.vite_dev_server,
428 self.config.entry_point,
429 page_json
430 )
431 } else {
432 format!(
433 r#"<!DOCTYPE html>
434<html lang="en">
435<head>
436 <meta charset="UTF-8">
437 <meta name="viewport" content="width=device-width, initial-scale=1.0">
438 <meta name="csrf-token" content="{csrf}">
439 <title>Inertia App</title>
440 <script type="module" src="/assets/main.js"></script>
441 <link rel="stylesheet" href="/assets/main.css">
442</head>
443<body>
444 <div id="app" data-page="{page_json}"></div>
445</body>
446</html>"#
447 )
448 };
449
450 InertiaHttpResponse::html(html)
451 }
452}