inertia_rust/
props.rs

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    /// Converts a serializeable object into a [`serde_json::Value`]. If it fails,
19    /// automatically maps the error to [`InertiaError`].
20    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    /// - ALWAYS included on standard visits
32    /// - OPTIONALLY included on partial reloads
33    /// - ALWAYS evaluated
34    Data(Result<Value, InertiaError>),
35    /// - ALWAYS included on standard visits
36    /// - OPTIONALLY included on partial reloads
37    /// - ONLY evaluated when included
38    Lazy(PropResolver),
39    /// - ALWAYS included on standard visits
40    /// - ALWAYS included on partial reloads (even if not requested or excepted)
41    /// - ALWAYS evaluated
42    Always(Result<Value, InertiaError>),
43    /// - NEVER included on standard visits
44    /// - OPTIONALLY included on partial reloads
45    /// - ONLY evaluated when needed
46    Demand(PropResolver),
47    /// Exactly the same as `InertiaProp::Demand`, except that it will be automatically
48    /// fetched by Inertia when the page is first loaded.
49    ///
50    /// Refer to [Deferred Props](https://inertiajs.com/deferred-props) for
51    /// more details.
52    Deferred(PropResolver, Option<&'a str>),
53    /// Make a property mergeable. Refer to [Merging Props](https://inertiajs.com/merging-props)
54    /// documentation for more details.
55    ///
56    /// It can only hold the `InertiaProp::Data` and `InertiaProp::Deferred` variants of `InertiaProp`.
57    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    /// Converts an `InertiaProp to `InertiaMergeableProp`.
82    ///
83    /// # Panics
84    /// Will panic if the prop isn't neither `InertiaProp::Data` nor `InertiaProp::Deferred`
85    /// variants.
86    #[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        // Request headers
321        // X-Inertia: true
322        // X-Inertia-Version: generated_version
323        // X-Inertia-Partial-Data: events
324        // X-Inertia-Partial-Component: Events
325        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                // "categories": ["foo", "bar"],                // NOT included
348                "events": [{"id": 1, "title": "Baile"}],      // included and evaluated
349                "auth": { "name": "John Doe" },              // ALWAYS included
350            },
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" },              // ALWAYS included
384            // "categories": ["foo", "bar"],                // NOT included
385            // "events": [{"id": 1, "title": "Baile"}]      // NOT included NOR EVALUATED
386            },
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            // "radioStatus": { "announcer": "John Doe" },  // NOT included
426            "categories": ["foo", "bar"],                   // included
427            },
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        // partial request for 'default' group only contains 'events' prop
491        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        //
680        // second page
681        //
682        *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}