quarb_session/lib.rs
1//! The backend-agnostic interactive Quarb session.
2//!
3//! The session logic — the `&N` / `&N!` / `&N#` macro history, its
4//! resolution, and rendering coordination — is pure and depends on
5//! two pluggable seams:
6//!
7//! - an [`Executor`]: where the arbor materializes and queries run.
8//! [`LocalExecutor`] runs in-process (the native full fleet, or the
9//! wasm text-format subset); a daemon executor (native, elsewhere)
10//! proxies to a resident process.
11//! - a [`Store`]: where the session's durable state persists — a file
12//! store on native, a browser store (localStorage) on wasm, or
13//! [`MemStore`] for none.
14//!
15//! Results cross those seams as [`Cell`]s — node results already
16//! rendered to their locator string, value results keeping their
17//! type — because a raw `NodeId` is meaningless across a socket or
18//! the JS boundary.
19
20pub mod doc;
21mod local;
22mod session;
23mod store;
24
25pub use doc::{Doc, Options};
26
27/// One mount source: a path, optionally under an explicit mount name
28/// (the `NAME=TARGET` spelling); unnamed sources mount under their
29/// file stem.
30#[derive(Clone, Debug)]
31pub struct MountSpec {
32 pub name: Option<String>,
33 pub path: std::path::PathBuf,
34}
35
36impl MountSpec {
37 /// Parse a CLI argument: `NAME=TARGET` when the left side is a
38 /// plain mount name (letters, digits, `_`, `-`), else a bare
39 /// path.
40 pub fn parse(arg: &str) -> MountSpec {
41 if let Some((name, target)) = arg.split_once('=')
42 && !name.is_empty()
43 && !target.is_empty()
44 && name
45 .chars()
46 .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
47 {
48 return MountSpec {
49 name: Some(name.to_string()),
50 path: std::path::PathBuf::from(target),
51 };
52 }
53 MountSpec {
54 name: None,
55 path: std::path::PathBuf::from(arg),
56 }
57 }
58}
59pub use local::LocalExecutor;
60pub use session::Session;
61pub use store::{MemStore, SessionState, Store};
62#[cfg(feature = "native")]
63pub use store::FileStore;
64
65use quarb::Value;
66
67/// One result row, rendered so it can cross a process or JS boundary:
68/// a node as its locator string, a value keeping its type.
69#[derive(Clone, Debug)]
70pub enum Cell {
71 Node(String),
72 Value(Value),
73}
74
75impl Cell {
76 /// The line as printed in a terminal.
77 pub fn display(&self) -> String {
78 match self {
79 Cell::Node(s) => s.clone(),
80 Cell::Value(v) => v.to_string(),
81 }
82 }
83}
84
85/// The materialization + query substrate. Implementations run a query
86/// against a standing arbor and return rendered [`Cell`]s.
87///
88/// `query` is the whole text to run — the session prepends its macro
89/// table (`def &N: …;`) so history resolves inline, which also lets it
90/// cross a process boundary (the daemon) as plain text.
91pub trait Executor {
92 fn run(&self, query: &str) -> anyhow::Result<Vec<Cell>>;
93
94 /// Run against a *freshly re-materialized* source — the `&N!`
95 /// live reading. The default re-runs against the standing arbor
96 /// (an immutable source never drifts); executors that can re-open
97 /// the source override it so a live reading sees current data,
98 /// diverging from the frozen `&N#`.
99 fn run_fresh(&self, query: &str) -> anyhow::Result<Vec<Cell>> {
100 self.run(query)
101 }
102}
103
104#[cfg(feature = "native")]
105mod daemon;
106#[cfg(feature = "native")]
107pub use daemon::DaemonExecutor;
108
109#[cfg(test)]
110mod mount_spec_tests {
111 use super::MountSpec;
112
113 #[test]
114 fn name_target_splits() {
115 let spec = MountSpec::parse("t=data/titanic.csv");
116 assert_eq!(spec.name.as_deref(), Some("t"));
117 assert_eq!(spec.path.to_str(), Some("data/titanic.csv"));
118 let spec = MountSpec::parse("births-2=x.json");
119 assert_eq!(spec.name.as_deref(), Some("births-2"));
120 }
121
122 #[test]
123 fn bare_paths_stay_bare() {
124 for arg in [
125 "titanic.csv",
126 "git:.",
127 // a left side that isn't a plain mount name is path text
128 "a/b=c.json",
129 "=x.json",
130 "name=",
131 ] {
132 let spec = MountSpec::parse(arg);
133 assert!(spec.name.is_none(), "{arg} should not split");
134 assert_eq!(spec.path.to_str(), Some(arg));
135 }
136 }
137}