whiskers.git / crates / whiskers-core / src / entrance.rs
1//! Whether the child's side of the app may show: the first-run gate.
2//!
3//! The child's name and age are entered on the parents' side and nowhere else, and everything
4//! Whiskers says is written for them (the greeting included), so a device that has no
5//! [`Child`] has nothing it may say yet. Until both halves are filled in the child's side stays
6//! shut and the app opens on the parents' "About your child" screen.
7//!
8//! "No profile" is judged **after a sync with the service**, not before: a reinstall that gets the
9//! household document back is not a first run. So the decision has three answers. With a profile
10//! the child's side opens at once (a device that already knows who it talks to never waits for the
11//! network). With none and no sync tried yet, the shell is told to sync first
12//! ([`Entrance::SyncFirst`]). With none after a sync, or after a sync that could not reach the
13//! service, it is a first run; [`Entrance::FirstRun::service_reached`] says which, so the screen can
14//! tell a parent that the fields will be shared once the service is back.
15//!
16//! The guard's floor is unchanged ([`Age::YOUNGEST`](crate::Age::YOUNGEST) with no profile). This
17//! gate is why it is not reachable in normal use, not a replacement for it.
18
19use crate::profile::Child;
20
21/// What the shell knows about syncing with the service, when it asks.
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum SyncOutcome {
24    /// No sync has been tried since the app opened.
25    NotTried,
26    /// The service answered; whatever it holds has been merged in.
27    Reached,
28    /// The service could not be reached (offline, down, no address that works).
29    Unreachable,
30}
31
32/// Where the app opens.
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum Entrance {
35    /// A profile exists: the child's side may show.
36    ChildSide,
37    /// There is no profile and no sync has been tried: try one, then ask again.
38    SyncFirst,
39    /// There is still no profile after trying: the child's side stays shut and the parents fill in
40    /// the name and the age.
41    FirstRun { service_reached: bool },
42}
43
44impl Entrance {
45    /// The one place the question is answered. `child` is the profile this device holds now.
46    pub fn of(child: Option<&Child>, sync: SyncOutcome) -> Entrance {
47        let entrance = match (child, sync) {
48            (Some(_), _) => Entrance::ChildSide,
49            (None, SyncOutcome::NotTried) => Entrance::SyncFirst,
50            (None, SyncOutcome::Reached) => Entrance::FirstRun { service_reached: true },
51            (None, SyncOutcome::Unreachable) => Entrance::FirstRun { service_reached: false },
52        };
53        ::log::info!("entrance: profile set = {}, sync {sync:?} -> {entrance:?}", child.is_some());
54        entrance
55    }
56
57    /// Whether the child's side may show. The only way to be told yes is a profile.
58    pub fn child_side_open(self) -> bool {
59        self == Entrance::ChildSide
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use crate::profile::{Age, ChildName};
67
68    fn ada() -> Child {
69        Child { name: ChildName::new("Ada").unwrap(), age: Age::new(6).unwrap() }
70    }
71
72    #[test]
73    fn a_profile_opens_the_child_side_without_waiting_for_the_network() {
74        for sync in [SyncOutcome::NotTried, SyncOutcome::Reached, SyncOutcome::Unreachable] {
75            assert_eq!(Entrance::of(Some(&ada()), sync), Entrance::ChildSide, "{sync:?}");
76        }
77    }
78
79    #[test]
80    fn no_profile_and_no_sync_yet_means_sync_first() {
81        assert_eq!(Entrance::of(None, SyncOutcome::NotTried), Entrance::SyncFirst);
82    }
83
84    #[test]
85    fn no_profile_after_reaching_the_service_is_a_first_run() {
86        assert_eq!(Entrance::of(None, SyncOutcome::Reached), Entrance::FirstRun { service_reached: true });
87    }
88
89    #[test]
90    fn no_profile_with_the_service_unreachable_is_still_a_first_run_and_says_so() {
91        assert_eq!(Entrance::of(None, SyncOutcome::Unreachable), Entrance::FirstRun { service_reached: false });
92    }
93
94    #[test]
95    fn only_a_profile_opens_the_child_side() {
96        assert!(Entrance::ChildSide.child_side_open());
97        assert!(!Entrance::SyncFirst.child_side_open());
98        assert!(!Entrance::FirstRun { service_reached: true }.child_side_open());
99        assert!(!Entrance::FirstRun { service_reached: false }.child_side_open());
100    }
101}