1use crate::{
2 error::IntoInertiaError,
3 page::DeferredProps,
4 req_type::{InertiaRequestType, PartialComponent},
5 InertiaError,
6};
7use serde::Serialize;
8use serde_json::{to_value, Map, Value};
9use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc};
10
11type PropResolver = Arc<
12 dyn Fn() -> Pin<Box<dyn Future<Output = Result<Value, InertiaError>> + Send>> + Send + Sync,
13>;
14
15pub type InertiaProps<'a> = HashMap<&'a str, InertiaProp<'a>>;
16
17pub trait IntoInertiaPropResult {
18 fn into_inertia_value(self) -> Result<Value, InertiaError>;
21}
22
23impl<T: Serialize> IntoInertiaPropResult for T {
24 fn into_inertia_value(self) -> Result<Value, InertiaError> {
25 to_value(self).map_err(IntoInertiaError::into_inertia_error)
26 }
27}
28
29#[derive(Clone)]
30pub enum InertiaProp<'a> {
31 Data(Result<Value, InertiaError>),
35 Lazy(PropResolver),
39 Always(Result<Value, InertiaError>),
43 Demand(PropResolver),
47 Deferred(PropResolver, Option<&'a str>),
53 Mergeable(Box<InertiaProp<'a>>),
58}
59
60impl<'a> InertiaProp<'a> {
61 #[inline]
62 pub(crate) async fn resolve_unconditionally(self) -> Result<Value, InertiaError> {
63 match self {
64 InertiaProp::Always(value) => value,
65 InertiaProp::Data(value) => value,
66 InertiaProp::Demand(resolver) => resolver()
67 .await
68 .map_err(IntoInertiaError::into_inertia_error),
69 InertiaProp::Deferred(resolver, _group) => resolver()
70 .await
71 .map_err(IntoInertiaError::into_inertia_error),
72 InertiaProp::Mergeable(prop) => Box::pin(prop.resolve_unconditionally())
73 .await
74 .map_err(IntoInertiaError::into_inertia_error),
75 InertiaProp::Lazy(resolver) => resolver()
76 .await
77 .map_err(IntoInertiaError::into_inertia_error),
78 }
79 }
80
81 #[allow(dead_code)]
87 pub fn into_mergeable(self) -> InertiaProp<'a> {
88 match self {
89 InertiaProp::Data(_) | InertiaProp::Deferred(_, _) => (),
90 _ => panic!("You've tried to convert an invalid variant of InertiaProp into InertiaMergeableProp."),
91 }
92
93 InertiaProp::Mergeable(Box::new(self))
94 }
95
96 pub fn data<T>(value: T) -> InertiaProp<'a>
97 where
98 T: Serialize,
99 {
100 InertiaProp::Data(value.into_inertia_value())
101 }
102
103 pub fn always<T>(value: T) -> InertiaProp<'a>
104 where
105 T: Serialize,
106 {
107 InertiaProp::Always(value.into_inertia_value())
108 }
109
110 pub fn merge<T>(value: T) -> InertiaProp<'a>
111 where
112 T: Serialize,
113 {
114 let prop = InertiaProp::Data(value.into_inertia_value());
115 InertiaProp::Mergeable(Box::new(prop))
116 }
117
118 pub fn lazy(resolver: PropResolver) -> InertiaProp<'a> {
119 InertiaProp::Lazy(resolver)
120 }
121
122 pub fn demand(resolver: PropResolver) -> InertiaProp<'a> {
123 InertiaProp::Demand(resolver)
124 }
125
126 pub fn defer(resolver: PropResolver) -> InertiaProp<'a> {
127 InertiaProp::Deferred(resolver, None)
128 }
129
130 pub fn defer_with_group(resolver: PropResolver, group: &'a str) -> InertiaProp<'a> {
131 InertiaProp::Deferred(resolver, Some(group))
132 }
133}
134
135#[inline]
136pub(crate) async fn resolve_props<'a>(
137 raw_props: &'a InertiaProps<'a>,
138 req_type: &InertiaRequestType,
139) -> Result<Map<String, Value>, InertiaError> {
140 let mut props = Map::new();
141
142 match req_type {
143 InertiaRequestType::Standard => {
144 for (key, prop) in raw_props.iter() {
145 if matches!(prop, InertiaProp::Demand(_) | InertiaProp::Deferred(_, _)) {
146 continue;
147 }
148
149 if let InertiaProp::Mergeable(prop) = prop {
150 if matches!(**prop, InertiaProp::Deferred(_, _)) {
151 continue;
152 }
153 }
154
155 match prop.clone().resolve_unconditionally().await {
156 Ok(value) => {
157 props.insert(key.to_string(), value);
158 }
159
160 Err(err) => {
161 log::error!("Failed to resolve prop {}: {}", key, err);
162 }
163 }
164 }
165 }
166
167 InertiaRequestType::Partial(partial) => {
168 for (key, prop) in raw_props {
169 let key = key.to_string();
170
171 if !matches!(prop, InertiaProp::Always(_)) && !should_be_pushed(&key, partial) {
172 continue;
173 }
174
175 match prop {
176 InertiaProp::Always(value) | InertiaProp::Data(value) => {
177 let value = value.clone().map_err(|err| {
178 log::error!("Failed to resolve prop \"{}\": {}", &key, err);
179 err
180 })?;
181
182 props.insert(key, value);
183 }
184
185 InertiaProp::Lazy(resolver)
186 | InertiaProp::Demand(resolver)
187 | InertiaProp::Deferred(resolver, _) => {
188 let value = resolver().await.map_err(|err| {
189 log::error!("Failed to resolve prop callback \"{}\": {}", &key, err);
190 err
191 })?;
192
193 props.insert(key, value);
194 }
195
196 InertiaProp::Mergeable(prop) => {
197 let value =
198 prop.clone()
199 .resolve_unconditionally()
200 .await
201 .map_err(|err| {
202 log::error!(
203 "Failed to resolve mergeable prop \"{}\": {}",
204 &key,
205 err
206 );
207 err
208 })?;
209
210 props.insert(key, value);
211 }
212 };
213 }
214 }
215 };
216
217 Ok(props)
218}
219
220#[inline]
221fn should_be_pushed(key: &String, partial: &PartialComponent) -> bool {
222 partial.only.contains(key) && !partial.except.contains(key)
223}
224
225#[inline]
226pub fn get_mergeable_props<'b>(
227 props: &'b InertiaProps<'b>,
228 keys_to_reset: Vec<&'b str>,
229) -> Option<Vec<&'b str>> {
230 let props = props
231 .iter()
232 .filter(|(key, prop)| {
233 matches!(**prop, InertiaProp::Mergeable(_)) && !keys_to_reset.contains(*key)
234 })
235 .map(|(key, _)| *key)
236 .collect::<Vec<_>>();
237
238 match props.is_empty() {
239 true => None,
240 false => Some(props),
241 }
242}
243
244#[inline]
245pub fn get_deferred_props<'b>(
246 props: &'b InertiaProps<'b>,
247 req_type: &InertiaRequestType,
248) -> DeferredProps<'b> {
249 if req_type.is_partial() {
250 return None;
251 }
252
253 let mut deferred_props = HashMap::new();
254
255 for (key, prop) in props.iter() {
256 let group;
257
258 if let &InertiaProp::Deferred(_, _group) = prop {
259 group = _group.unwrap_or("default");
260 } else if let InertiaProp::Mergeable(prop) = prop {
261 if let InertiaProp::Deferred(_, _group) = &**prop {
262 group = _group.unwrap_or("default");
263 } else {
264 continue;
265 }
266 } else {
267 continue;
268 }
269
270 if !deferred_props.contains_key(group) {
271 deferred_props.insert(group, vec![*key]);
272 } else {
273 deferred_props.get_mut(group).unwrap().push(*key);
274 }
275 }
276
277 match deferred_props.is_empty() {
278 true => None,
279 false => Some(deferred_props),
280 }
281}
282
283#[cfg(test)]
284mod test {
285 use crate::props::{get_deferred_props, get_mergeable_props, InertiaProp};
286 use crate::req_type::{InertiaRequestType, PartialComponent};
287 use crate::{hashmap, prop_resolver, Component, InertiaPage, IntoInertiaPropResult};
288 use actix_web::test;
289 use serde::Serialize;
290 use serde_json::{json, to_value, Value};
291 use std::collections::HashMap;
292 use std::sync::{Arc, Mutex};
293
294 use super::resolve_props;
295
296 #[test]
297 async fn test_inertia_partials_visit_page() {
298 let lazy_evaluation_counter = Arc::new(Mutex::new(0));
299 let counter_clone = lazy_evaluation_counter.clone();
300
301 #[derive(Serialize)]
302 struct Events {
303 id: u16,
304 title: String,
305 }
306
307 let props = hashmap![
308 "auth" => InertiaProp::always(json!({"name": "John Doe"})),
309 "categories" => InertiaProp::Data(Ok(vec!["foo".to_string(),"bar".to_string()].into())),
310 "events" => InertiaProp::lazy(prop_resolver!(let counter = counter_clone.clone(); {
311 *counter.lock().unwrap() += 1;
312 let event = Events {
313 id: 1,
314 title: "Baile".into(),
315 };
316 vec![event].into_inertia_value()
317 }))
318 ];
319
320 let req_type = InertiaRequestType::Partial(PartialComponent {
326 component: Component("Events".to_string()),
327 only: Vec::from(["events".to_string()]),
328 except: Vec::new(),
329 });
330
331 let page = InertiaPage::new(
332 Component("Events".into()),
333 "/events/80",
334 Some("generated_version"),
335 resolve_props(&props, &req_type).await.unwrap(),
336 None,
337 None,
338 false,
339 false,
340 );
341
342 let json_page_example = json!({
343 "clearHistory":false,
344 "component": "Events",
345 "encryptHistory":false,
346 "props": {
347 "events": [{"id": 1, "title": "Baile"}], "auth": { "name": "John Doe" }, },
351 "url": "/events/80",
352 "version": "generated_version",
353
354 });
355
356 assert_eq!(
357 json!(page).to_string(),
358 serde_json::to_string(&json_page_example).unwrap(),
359 );
360
361 let req_type = InertiaRequestType::Partial(PartialComponent {
362 component: Component("Events".to_string()),
363 only: Vec::new(),
364 except: Vec::new(),
365 });
366
367 let page = InertiaPage::new(
368 Component("Events".into()),
369 "/events/80",
370 Some("generated_version"),
371 resolve_props(&props, &req_type).await.unwrap(),
372 None,
373 None,
374 false,
375 false,
376 );
377
378 let json_page_example = json!({
379 "clearHistory":false,
380 "component": "Events",
381 "encryptHistory":false,
382 "props": {
383 "auth": { "name": "John Doe" }, },
387 "url": "/events/80",
388 "version": "generated_version",
389
390 });
391
392 assert_eq!(
393 json!(page).to_string(),
394 serde_json::to_string(&json_page_example).unwrap(),
395 );
396
397 assert_eq!(*lazy_evaluation_counter.lock().unwrap(), 1);
398 }
399
400 #[test]
401 async fn test_inertia_standard_visit_page() {
402 let props = hashmap! [
403 "radioStatus" => InertiaProp::Demand(Arc::new(|| Box::pin(async move { Ok(json!({"announcer":"John Doe"})) }))),
404 "categories" => InertiaProp::data(vec!["foo".to_string(), "bar".to_string()])
405 ];
406
407 let req_type = InertiaRequestType::Standard;
408
409 let page = InertiaPage::new(
410 Component("Categories".into()),
411 "/categories",
412 Some("generated_version"),
413 resolve_props(&props, &req_type).await.unwrap(),
414 None,
415 None,
416 false,
417 false,
418 );
419
420 let json_page_example = json!({
421 "clearHistory": false,
422 "component": "Categories",
423 "encryptHistory": false,
424 "props": {
425 "categories": ["foo", "bar"], },
428 "url": "/categories",
429 "version": "generated_version"
430 });
431
432 assert_eq!(
433 json!(page).to_string(),
434 serde_json::to_string(&json_page_example).unwrap(),
435 );
436 }
437
438 fn get_deferred_props_hashmap<'a>() -> HashMap<&'a str, InertiaProp<'a>> {
439 hashmap![
440 "users" => InertiaProp::Deferred(prop_resolver!({ vec!["user1", "user2", "user3"].into_inertia_value() }), Some("users")),
441 "permissions" => InertiaProp::Deferred(prop_resolver!({ vec!["delete", "update", "read"].into_inertia_value() }), Some("users")),
442 "events" => InertiaProp::Deferred(prop_resolver!({ vec!["event1", "event2", "event3"].into_inertia_value() }), None)
443 ]
444 }
445
446 #[test]
447 async fn test_standard_request_deferred_props_behavior() {
448 let props = get_deferred_props_hashmap();
449
450 let standard_page = json!(InertiaPage {
451 deferred_props: get_deferred_props(&props, &InertiaRequestType::Standard),
452 component: "Foo".into(),
453 clear_history: false,
454 encrypt_history: false,
455 merge_props: None,
456 props: resolve_props(&props, &InertiaRequestType::Standard)
457 .await
458 .unwrap(),
459 url: "foo",
460 version: Some("foo")
461 });
462
463 assert!(
464 standard_page.clone()["deferredProps"]["default"]
465 .as_array()
466 .unwrap()
467 .contains(&to_value("events").unwrap()),
468 "Deferred Props field from standard visit should contain an 'default' gorup containing 'events' key."
469 );
470
471 assert!([
472 to_value("users").unwrap(),
473 to_value("permissions").unwrap()
474 ]
475 .iter()
476 .all(|key| standard_page.clone()["deferredProps"]["users"]
477 .as_array()
478 .unwrap()
479 .contains(key)),
480 "Deferred Props field from standard visit should contain an 'user' group containing 'users' and 'permissions' keys."
481 );
482
483 assert!(standard_page["props"].as_object().unwrap().is_empty(), "Props field should be empty once there is only deferred props in it and it's an standard request.");
484 }
485
486 #[test]
487 async fn test_partial_request_for_default_group_from_deferred_props_behavior() {
488 let props = get_deferred_props_hashmap();
489
490 let partial_req_for_default = InertiaRequestType::Partial(PartialComponent {
492 component: "Foo".into(),
493 only: vec!["events".into()],
494 except: vec![],
495 });
496
497 let default_partial_page = json!(InertiaPage {
498 deferred_props: get_deferred_props(&props, &partial_req_for_default),
499 component: "Foo".into(),
500 clear_history: false,
501 encrypt_history: false,
502 merge_props: None,
503 props: resolve_props(&props, &partial_req_for_default)
504 .await
505 .unwrap(),
506 url: "foo",
507 version: Some("foo")
508 });
509
510 assert!(
511 default_partial_page.get("deferredProps").is_none(),
512 "'deferredProps' field should not exist in partial requests."
513 );
514
515 assert!(
516 default_partial_page["props"]
517 .as_object()
518 .unwrap()
519 .get("events")
520 .is_some_and(
521 |events| ["event1", "event2", "event3"].iter().all(|event| events
522 .as_array()
523 .unwrap()
524 .contains(&Value::String(event.to_string())))
525 ),
526 "partial request for 'default' group should contain 'events' list in 'props' field with the props events values."
527 )
528 }
529
530 #[test]
531 async fn test_partial_request_for_users_group_from_deferred_props_behavior() {
532 let props = get_deferred_props_hashmap();
533
534 let partial_req_for_users = InertiaRequestType::Partial(PartialComponent {
535 component: "Foo".into(),
536 only: vec!["users".into(), "permissions".into()],
537 except: vec![],
538 });
539
540 let users_partial_page = json!(InertiaPage {
541 deferred_props: get_deferred_props(&props, &partial_req_for_users),
542 component: "Foo".into(),
543 clear_history: false,
544 encrypt_history: false,
545 merge_props: None,
546 props: resolve_props(&props, &partial_req_for_users).await.unwrap(),
547 url: "foo",
548 version: Some("foo")
549 });
550
551 assert!(
552 users_partial_page.get("deferredProps").is_none(),
553 "'deferredProps' field should not exist in partial requests."
554 );
555
556 assert!(users_partial_page["props"]
557 .as_object()
558 .unwrap()
559 .get("users")
560 .is_some_and(
561 |users| ["user1", "user2", "user3"].iter().all(|user| users
562 .as_array()
563 .unwrap()
564 .contains(&to_value(user).unwrap()))
565 ),
566 "'props' field should contain an 'users' group which should be a list containing the values from given props hashmap 'users' field."
567 );
568
569 assert!(users_partial_page["props"]
570 .as_object()
571 .unwrap()
572 .get("permissions")
573 .is_some_and(
574 |permissions| ["delete", "update", "read"].iter().all(|permission| permissions
575 .as_array()
576 .unwrap()
577 .contains(&to_value(permission).unwrap()))
578 ),
579 "'props' field should contain an 'permissions' group which should be a list containing the values from given props hashmap 'permissions' field."
580 );
581 }
582
583 #[test]
584 async fn test_mergeable_props_behavior_without_reset_list() {
585 let get_inertia_pages = move |page: usize| {
586 let users_memory_db = Arc::new(vec!["user1", "user2", "user3", "user4", "user5"]);
587 let permissions_memory_db = ["read", "update", "delete"];
588
589 let props = hashmap![
590 "permissions" => InertiaProp::Mergeable(Box::new(InertiaProp::Data(
591 permissions_memory_db.iter().skip((page -1) * 2).take(2).cloned().collect::<Vec<_>>().into_inertia_value()
592 ))),
593 "users" => InertiaProp::defer(prop_resolver!(
594 let users = users_memory_db.clone();
595 {
596 users
597 .clone()
598 .iter()
599 .skip((page - 1) * 3)
600 .take(3)
601 .cloned()
602 .collect::<Vec<_>>()
603 .into_inertia_value()
604 }))
605 .into_mergeable()
606 ];
607
608 let partial_req = InertiaRequestType::Partial(PartialComponent {
609 component: "Foo".into(),
610 except: vec![],
611 only: vec!["users".into()],
612 });
613
614 async move {
615 (
616 json!(InertiaPage {
617 clear_history: false,
618 encrypt_history: false,
619 component: "Foo".into(),
620 deferred_props: get_deferred_props(&props, &InertiaRequestType::Standard),
621 merge_props: get_mergeable_props(&props, vec![]),
622 props: resolve_props(&props, &InertiaRequestType::Standard)
623 .await
624 .unwrap(),
625 url: "",
626 version: Some("")
627 }),
628 json!(InertiaPage {
629 clear_history: false,
630 encrypt_history: false,
631 component: "Foo".into(),
632 deferred_props: get_deferred_props(&props, &partial_req),
633 merge_props: get_mergeable_props(&props, vec![]),
634 props: resolve_props(&props, &partial_req).await.unwrap(),
635 url: "",
636 version: Some("")
637 }),
638 )
639 }
640 };
641
642 let page = Arc::new(Mutex::new(1));
643 let _page = *page.lock().unwrap();
644 let (standard_page, partial_page) = get_inertia_pages(_page).await;
645
646 assert!(standard_page["props"]
647 .as_object()
648 .unwrap()
649 .contains_key("permissions"));
650
651 assert!(partial_page["props"]
652 .as_object()
653 .unwrap()
654 .contains_key("users"));
655
656 assert!(["permissions", "users"]
657 .iter()
658 .all(|prop| standard_page["mergeProps"]
659 .as_array()
660 .is_some_and(|props| props.contains(&to_value(prop).unwrap()))));
661
662 assert!(standard_page["deferredProps"]["default"]
663 .as_array()
664 .unwrap()
665 .contains(&to_value("users").unwrap()));
666
667 assert!(["user1", "user2", "user3"]
668 .iter()
669 .all(|user| partial_page["props"]["users"]
670 .as_array()
671 .is_some_and(|users| users.contains(&to_value(user).unwrap()))));
672
673 assert!(["read", "update"]
674 .iter()
675 .all(|permission| standard_page["props"]["permissions"]
676 .as_array()
677 .is_some_and(|permissions| permissions.contains(&to_value(permission).unwrap()))));
678
679 *page.lock().unwrap() = 2;
683 let _page = *page.lock().unwrap();
684 let (standard_page, partial_page) = get_inertia_pages(_page).await;
685
686 log::info!("{}\n\n", standard_page);
687 log::info!("{}\n\n", partial_page);
688
689 assert!(partial_page["props"]
690 .as_object()
691 .unwrap()
692 .contains_key("users"));
693
694 assert!(standard_page["props"]
695 .as_object()
696 .unwrap()
697 .contains_key("permissions"));
698
699 assert!(["permissions", "users"]
700 .iter()
701 .all(|prop| standard_page["mergeProps"]
702 .as_array()
703 .is_some_and(|props| props.contains(&to_value(prop).unwrap()))));
704
705 assert!(standard_page["deferredProps"]["default"]
706 .as_array()
707 .unwrap()
708 .contains(&to_value("users").unwrap()));
709
710 assert!(["user4", "user5"]
711 .iter()
712 .all(|user| partial_page["props"]["users"]
713 .as_array()
714 .is_some_and(|users| users.contains(&to_value(user).unwrap()))));
715
716 assert!(standard_page["props"]["permissions"]
717 .as_array()
718 .is_some_and(|permissions| permissions.eq(&["delete"])));
719 }
720
721 #[test]
722 async fn test_mergeable_props_behavior_with_reset() {
723 async fn get_inertia_page(page: usize, keys_to_reset: &[&str]) -> Value {
724 let permissions_mem_db = ["read", "update", "delete"];
725 let per_page = 2;
726
727 let props = hashmap![
728 "permissions" => InertiaProp::Data(
729 permissions_mem_db
730 .iter()
731 .skip((page -1) * per_page)
732 .take(per_page)
733 .cloned()
734 .collect::<Vec<_>>()
735 .into_inertia_value())
736 .into_mergeable()
737 ];
738
739 json!(InertiaPage {
740 clear_history: false,
741 encrypt_history: false,
742 component: "Foo".into(),
743 deferred_props: None,
744 merge_props: get_mergeable_props(&props, keys_to_reset.to_vec()),
745 props: resolve_props(&props, &InertiaRequestType::Standard)
746 .await
747 .unwrap(),
748 url: "",
749 version: None,
750 })
751 }
752
753 let page = Arc::new(Mutex::new(1));
754 let _page = *page.lock().unwrap();
755 let inertia_page = get_inertia_page(_page, &[]).await;
756
757 assert!(inertia_page["mergeProps"]
758 .as_array()
759 .unwrap()
760 .contains(&to_value("permissions").unwrap()));
761
762 assert!(["read", "update"]
763 .iter()
764 .all(|permission| inertia_page["props"]["permissions"]
765 .as_array()
766 .unwrap()
767 .contains(&to_value(permission).unwrap())));
768
769 *page.lock().unwrap() = 2;
770 let _page = *page.lock().unwrap();
771 let inertia_page = get_inertia_page(_page, &["permissions"]).await;
772
773 assert!(inertia_page.get("mergeProps").is_none());
774
775 assert!(inertia_page["props"]["permissions"]
776 .as_array()
777 .unwrap()
778 .eq(&["delete"]));
779 }
780}