rig_harness/plugin_data.rs
1//! Where a plugin keeps files of its own, by how long they live:
2//! [`PluginData`], in every run. A plugin names its files under its own
3//! name, such as `vim/registers.json`.
4//!
5//! ```
6//! use rig_harness::prelude::*;
7//!
8//! fn save(data: Res<PluginData>) -> std::io::Result<()> {
9//! // Kept with the session.
10//! data.session.write_file("vim/registers.json", b"{}")?;
11//! // This project's, whichever session works in it.
12//! data.project.write_file("vim/marks.json", b"{}")?;
13//! // Every session's.
14//! data.user.write_file("vim/mode.json", b"\"normal\"")
15//! }
16//! ```
17
18use std::path::{Path, PathBuf};
19use std::sync::Arc;
20
21use bevy_ecs::prelude::*;
22use rig_core::tool::content_hash;
23use rig_harness_protocol::{Home, SessionDir};
24use rig_memory::{DirFiles, Files, MemoryStore, PluginFiles, SessionError};
25
26use crate::host::session::StoreOpener;
27
28/// Where plugins keep their files: per session, per project and per
29/// user. Not in the project's own directory: a plugin's data is the
30/// user's, never committed and never read from a checkout someone else
31/// made. Inserted once every plugin is built, in every run, unless the
32/// app has one: a run without a saved session keeps `session` in memory.
33#[derive(Resource, Clone)]
34pub struct PluginData {
35 /// This session's: `plugins/` in the session's store, which a delete
36 /// removes.
37 pub session: Arc<dyn Files>,
38 /// This project's, for this user, whichever session works in it:
39 /// `$RIG_HOME/projects/<name>-<key>/`, the key from the project's
40 /// canonical path, so every spelling of it shares one.
41 pub project: Arc<dyn Files>,
42 /// Across sessions and projects: `$RIG_HOME/data/`.
43 pub user: Arc<dyn Files>,
44}
45
46impl PluginData {
47 /// The places of a session kept in `session` (in memory with `None`),
48 /// working on the project at `root` in the workspace on `host` (`None`
49 /// for this machine), under `home`.
50 pub(crate) fn new(
51 home: &Home,
52 session: Option<Arc<dyn Files>>,
53 root: &Path,
54 host: Option<&str>,
55 ) -> Self {
56 let session = session.unwrap_or_else(|| Arc::new(MemoryStore::default()));
57 Self {
58 session: Arc::new(PluginFiles(session)),
59 project: Arc::new(DirFiles::new(project_dir(home, root, host))),
60 user: Arc::new(DirFiles::new(home.data())),
61 }
62 }
63}
64
65impl PluginData {
66 /// The plugin files of the saved session in `dir`, kept in the store
67 /// `stores` opens for it (`plugins/` of that store): what
68 /// [`PluginData::session`] is in a run of that session.
69 ///
70 /// # Errors
71 ///
72 /// When its store does not open.
73 pub fn of(stores: &StoreOpener, dir: &SessionDir) -> Result<Arc<dyn Files>, SessionError> {
74 let store: Arc<dyn Files> = stores.open(dir)?;
75 Ok(Arc::new(PluginFiles(store)))
76 }
77}
78
79/// The directory of `home` keeping plugins' files for the project at
80/// `root` on `host`: its name and the first 8 hex digits of the SHA-256
81/// of where it is, canonical on this machine.
82fn project_dir(home: &Home, root: &Path, host: Option<&str>) -> PathBuf {
83 let root = match host {
84 None => root.canonicalize().unwrap_or_else(|_| root.to_owned()),
85 Some(_) => root.to_owned(),
86 };
87 let mut place = host.map(|host| format!("{host}:")).unwrap_or_default();
88 place.push_str(&root.to_string_lossy());
89 let hash = content_hash(place.as_bytes()).0;
90 let name = root
91 .file_name()
92 .map(|name| name.to_string_lossy().into_owned())
93 .unwrap_or_default();
94 let key = hash.get(..8).unwrap_or(&hash);
95 home.projects().join(format!("{name}-{key}"))
96}
97
98#[cfg(test)]
99mod tests;