Skip to main content

slipcase_open/
outside.rs

1//! The three things the engine works against that are not its own state.
2//
3// Author: David M. Anderson
4// Built with AI assistance (Claude, Anthropic)
5//
6//! Where policy is read from, what opens a content file, and how the person is
7//! spoken to. They travel together because they are chosen together — once, by
8//! `main`, from what the machine turns out to be — and because the engine
9//! passes all three down the same path: concept 10 puts the policy decision
10//! immediately before the launch, and concept 5.1's warning is said about the
11//! same content file in the same breath.
12//!
13//! Held as trait objects rather than as type parameters. The alternative
14//! threads three generics through every signature from the front door down to
15//! the session, so that a program which picks its implementations at startup can
16//! pretend it knew them at compile time.
17
18use crate::platform::Launcher;
19use crate::policy::{self, Notify};
20use crate::present::{Channel, Report, Weight};
21
22/// What the engine has been given to work with.
23#[derive(Clone, Copy)]
24pub struct Outside<'a> {
25    /// Concept 10's layers, in whatever form this platform keeps them.
26    pub policy: &'a dyn policy::Source,
27    /// Concept 5 step 7, which hands the content file to the desktop.
28    pub launcher: &'a dyn Launcher,
29    /// Concept 9, which narrates and asks.
30    pub channel: &'a dyn Channel,
31    /// How much to say without being asked, resolved once when the instance
32    /// started. See [`Notify`].
33    pub notify: Notify,
34}
35
36impl<'a> Outside<'a> {
37    /// Gather the three, saying as much as [`Notify`]'s default allows.
38    #[must_use]
39    pub fn new(
40        policy: &'a dyn policy::Source,
41        launcher: &'a dyn Launcher,
42        channel: &'a dyn Channel,
43    ) -> Self {
44        Self {
45            policy,
46            launcher,
47            channel,
48            notify: Notify::default(),
49        }
50    }
51
52    /// The same, at the volume a resolved policy asked for.
53    #[must_use]
54    pub fn saying(mut self, notify: Notify) -> Self {
55        self.notify = notify;
56        self
57    }
58
59    /// Report, unless the threshold says this one is not worth an interruption.
60    ///
61    /// **Every routine report goes through here and no question does.** A
62    /// question is [`Channel::ask`], which this cannot reach, so no setting can
63    /// silence a session into stranding its content file.
64    pub fn report(&self, report: &Report) {
65        if report.weight != Weight::Routine || self.notify == Notify::Everything {
66            self.channel.report(report);
67        }
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::Outside;
74    use crate::platform::testing::Recording as Launching;
75    use crate::policy::{Notify, Origin, Read, Source};
76    use crate::present::testing::Recording as Told;
77    use crate::present::{Choice, Question, Report};
78
79    struct Default_;
80    impl Source for Default_ {
81        fn layer(&self, _o: Origin) -> Read {
82            Ok(None)
83        }
84    }
85
86    fn three() -> [Report; 3] {
87        [
88            Report::routine("a save landed"),
89            Report::ordinary("you asked and here is the answer"),
90            Report::interrupt("this one is a warning"),
91        ]
92    }
93
94    #[test]
95    fn the_default_drops_what_happened_on_its_own_and_nothing_else() {
96        let launcher = Launching::default();
97        let told = Told::default();
98        let outside = Outside::new(&Default_, &launcher, &told);
99        assert_eq!(outside.notify, Notify::Important);
100        for r in &three() {
101            outside.report(r);
102        }
103        let said = told.said();
104        assert!(!said.contains("a save landed"), "{said}");
105        assert!(said.contains("you asked"), "{said}");
106        assert!(said.contains("a warning"), "{said}");
107    }
108
109    #[test]
110    fn saying_everything_lets_the_routine_ones_through() {
111        let launcher = Launching::default();
112        let told = Told::default();
113        let outside = Outside::new(&Default_, &launcher, &told).saying(Notify::Everything);
114        for r in &three() {
115            outside.report(r);
116        }
117        assert_eq!(told.reports().len(), 3);
118    }
119
120    #[test]
121    fn no_setting_can_silence_a_question() {
122        // The property the whole design rests on. A question is `ask`, which
123        // `Outside::report` cannot reach, so a session waiting on a decision
124        // cannot be quietened into stranding its content file — and that is
125        // structural rather than a rule somebody has to keep in mind.
126        let launcher = Launching::default();
127        let told = Told::default();
128        let outside = Outside::new(&Default_, &launcher, &told);
129        outside.channel.ask(&Question {
130            about: "abc-0".into(),
131            summary: "report.pdf was left behind.".into(),
132            detail: Vec::new(),
133            choices: vec![Choice::WriteBack, Choice::Discard],
134        });
135        assert_eq!(told.questions().len(), 1);
136    }
137}