Skip to main content

tilo_app/framework/
hosted.rs

1//! An app's view side as a client of its one launcher-made connection to the
2//! session, over which it is handed readings and submits actions
3//! (`docs/architecture/apps.md` §3).
4//!
5//! An app whose records the session's document host keeps holds no document,
6//! no key and no transport of its own: the host runs its model and verbs,
7//! publishes the record's tree, answers a reading of it, and admits the
8//! actions a view submits (`docs/designs/app-language/one-app-model.md` §4).
9//! What the app's view side needs of that is two things, which this module
10//! names once:
11//!
12//! - **a reading in**: the state the session publishes for a resource, as
13//!   JSON ([`tilo_resource::Resource::state_json`],
14//!   [`tilo_resource::ResourceKind::to_json`]);
15//! - **an action out**: the action a control submits, with its arguments,
16//!   addressed to the record the session keeps.
17//!
18//! [`Connection`] is that pair as the app's process reaches it. The launcher
19//! makes the connection and the session knows which app and which launch is
20//! on it because it made it, so an action over it is decided by the session,
21//! never claimed by the app (§4). A host that runs the app inside itself (a
22//! test, a capture's fixture) supplies its own [`Answers`], and the view side
23//! is the same.
24//!
25//! The connection never carries a document, a key or another device's address:
26//! its reading is a state as JSON, and its action is a name and JSON
27//! arguments. Nothing here can reach anything else.
28
29use std::rc::Rc;
30
31use tilo_actions::ActionAddress;
32use tilo_resource::Resource;
33
34/// An action an app submits over its connection, with whom it is.
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct Ask {
37    /// The action's address: the resource it changes and its name.
38    pub address: ActionAddress,
39    /// Its arguments, by parameter name; empty for an action that takes none.
40    pub arguments: serde_json::Map<String, serde_json::Value>,
41    /// Whether the person was acting in this app's view when it was
42    /// submitted. An action without one is the app's own request, which the
43    /// session decides as the app (`docs/architecture/apps.md` §4).
44    pub in_person: bool,
45}
46
47impl Ask {
48    /// The action `action` of `resource`, with `arguments`, submitted by
49    /// `in_person`'s gesture — the person's, or the app's own request.
50    #[must_use]
51    pub fn new(
52        resource: impl Into<String>,
53        action: impl Into<String>,
54        arguments: serde_json::Map<String, serde_json::Value>,
55        in_person: bool,
56    ) -> Self {
57        Self {
58            address: ActionAddress::new(resource, action),
59            arguments,
60            in_person,
61        }
62    }
63}
64
65/// What answers a [`Connection`]: the session's door, or a host that runs the
66/// app inside itself in a test or a capture.
67pub trait Answers {
68    /// The reading of `resource` now, as the host publishes it: the state a
69    /// view reads, never a document. `None` when nothing publishes it, or the
70    /// caller may not read it (denial is absence, `§6`).
71    fn reading(&self, resource: &str) -> Option<serde_json::Value>;
72
73    /// Submit `ask`, answering the door's verdict.
74    ///
75    /// # Errors
76    /// The door's refusal, in its own words (`§4`, `§6`).
77    fn submit(&self, ask: &Ask) -> Result<(), String>;
78}
79
80/// The app's side of its connection to the session's door.
81#[derive(Clone)]
82pub struct Connection {
83    answers: Rc<dyn Answers>,
84}
85
86impl Connection {
87    /// The connection the launcher made the app it started: the session's
88    /// published readings and its action door, over the one connection the
89    /// launch holds. A process the session did not start reaches nothing
90    /// through it, as a launch it did not record is refused (`§3`).
91    #[must_use]
92    pub fn launcher() -> Self {
93        Self {
94            answers: Rc::new(Session),
95        }
96    }
97
98    /// A connection answered by `answers` in this process: a test's document
99    /// host, or a capture's readings.
100    #[must_use]
101    pub fn in_process(answers: impl Answers + 'static) -> Self {
102        Self {
103            answers: Rc::new(answers),
104        }
105    }
106
107    /// The reading of `resource` now.
108    #[must_use]
109    pub fn reading(&self, resource: &str) -> Option<serde_json::Value> {
110        self.answers.reading(resource)
111    }
112
113    /// The state of `R` that `resource` names, read from the host's reading
114    /// through `R`'s own model type: what a view reads while its app holds no
115    /// document.
116    ///
117    /// # Errors
118    /// Nothing publishes a reading the caller may read, or it is not `R`'s
119    /// state.
120    pub fn state<R: Resource>(&self, resource: &str) -> Result<R::State, String> {
121        let reading = self
122            .reading(resource)
123            .ok_or_else(|| format!("the session publishes no reading of {resource}"))?;
124        tilo_resource::state_from_reading::<R>(&reading).map_err(|error| error.to_string())
125    }
126
127    /// Submit `ask`; the door's verdict.
128    pub fn submit(&self, ask: &Ask) -> Result<(), String> {
129        self.answers.submit(ask)
130    }
131}
132
133impl std::fmt::Debug for Connection {
134    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        formatter.write_str("Connection")
136    }
137}
138
139/// The session's side, reached as any app reaches the session's published
140/// resources and its door (`ui_core::collaboration::SessionResource`,
141/// `ui_core::RemoteActions`).
142#[derive(Clone, Copy)]
143struct Session;
144
145impl Answers for Session {
146    fn reading(&self, resource: &str) -> Option<serde_json::Value> {
147        let reading = crate::framework::hosted_readings::reading(resource)?;
148        serde_json::from_str(&reading)
149            .inspect_err(|error| {
150                tracing::warn!(%error, %resource, "the session's reading of a resource is not JSON");
151            })
152            .ok()
153    }
154
155    fn submit(&self, ask: &Ask) -> Result<(), String> {
156        crate::framework::hosted_readings::submit(ask)
157    }
158}
159
160#[cfg(test)]
161mod tests;