tilo_app/framework/record_index.rs
1//! An app's records read without its window: what the system's search index
2//! asks of an app that has not opened the record.
3//!
4//! Search is a record's to answer, not a window's: a message, a conversation
5//! or an agent is found on a device that never ran the app, because whoever
6//! keeps the record reads it here and publishes what it holds to the same
7//! index a running window publishes to. The reader is the app's own binary in
8//! its `--index` mode: it links the app's model types, so what it derives is
9//! exactly what the app's window would, and the session that asks links none
10//! of them (`docs/architecture/apps.md`).
11//!
12//! The document arrives on standard input and leaves as the entries on
13//! standard output, as JSON: no path, no store and no window is handed in, so
14//! a reader cannot reach anything but the bytes it was asked about.
15
16use std::io::{Read as _, Write as _};
17
18use tilo_resource::{Doc, Kinds};
19use ui_core::search::index_publication::IndexEntry;
20
21use super::app::ResourceApp;
22use super::kinds::resource_kind;
23
24/// Every kind `A` declares, hosted by `A`: its resource, and the parts of the
25/// chains it keeps beside its record.
26///
27/// The same set [`super::kinds::register`] hosts for the process, built
28/// without depending on a definition having been created: a host that reads a
29/// record of an app that is not running reads it by these.
30fn kinds_of<A: ResourceApp>() -> Kinds {
31 let mut declared = super::Records::declaring();
32 A::records(&mut declared);
33 let mut kinds = Kinds::new();
34 kinds.insert(resource_kind::<A>());
35 for chain in declared.into_chains() {
36 kinds.insert(chain.hosted_by(A::ID, A::TITLE));
37 }
38 kinds
39}
40
41/// The flag that makes an app's binary print what a record holds for search
42/// instead of running: its document is read from standard input.
43pub const INDEX_FLAG: &str = "--index";
44
45/// What search finds in `doc`, as the index keeps it: every kind of `A`'s
46/// that the document holds, read the way its window reads the record.
47///
48/// A record kept before the app changed its storage is read at this build's
49/// version, in memory: the document itself is the app's to migrate.
50#[must_use]
51pub fn entries<A: ResourceApp>(doc: &Doc) -> Vec<IndexEntry> {
52 let mut found = Vec::new();
53 let mut keys = std::collections::BTreeSet::new();
54 for kind in kinds_of::<A>().iter() {
55 let mut read = doc.clone();
56 if kind.needs_migrate(&read) && kind.migrate(&mut read).is_err() {
57 continue;
58 }
59 let Ok(entries) = kind.search(&read) else {
60 // The document holds another kind: its own type answers for it.
61 continue;
62 };
63 for entry in entries {
64 let Some(entry) = super::root::index_entry(&entry).and_then(IndexEntry::bounded) else {
65 continue;
66 };
67 if keys.insert(entry.key.clone()) {
68 found.push(entry);
69 }
70 }
71 }
72 found
73}
74
75/// Answer the `--index` mode when the command line asks for it: read the
76/// record's document from standard input and print its entries. `None` for
77/// a command line that starts the app as usual.
78pub fn serve<A: ResourceApp>() -> Option<i32> {
79 let asked = std::env::args().skip(1).any(|arg| arg == INDEX_FLAG);
80 if !asked {
81 return None;
82 }
83 #[allow(
84 clippy::exit,
85 reason = "the record reader is the whole run of the process"
86 )]
87 Some(match ask::<A>() {
88 Ok(()) => 0,
89 Err(error) => {
90 eprintln!("Error: {error}");
91 1
92 }
93 })
94}
95
96/// [`serve`] once it is known to be asked: the document in, the entries out.
97fn ask<A: ResourceApp>() -> Result<(), String> {
98 let mut bytes = Vec::new();
99 std::io::stdin()
100 .lock()
101 .read_to_end(&mut bytes)
102 .map_err(|error| format!("the record is not read: {error}"))?;
103 let doc =
104 Doc::load(&bytes).map_err(|error| format!("the record is not a document: {error}"))?;
105 let entries = entries::<A>(&doc);
106 let mut out = Vec::new();
107 serde_json::to_writer(&mut out, &entries)
108 .map_err(|error| format!("the entries are not written: {error}"))?;
109 std::io::stdout()
110 .lock()
111 .write_all(&out)
112 .map_err(|error| format!("the entries do not leave: {error}"))
113}