Skip to main content

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            // The Quarb form for a record or list, bare scalars
81            // (ruling #51) — what qua prints.
82            Cell::Value(v) => v.display_form(),
83        }
84    }
85}
86
87/// The materialization + query substrate. Implementations run a query
88/// against a standing arbor and return rendered [`Cell`]s.
89///
90/// `query` is the whole text to run — the session prepends its macro
91/// table (`def &N: …;`) so history resolves inline, which also lets it
92/// cross a process boundary (the daemon) as plain text.
93pub trait Executor {
94    fn run(&self, query: &str) -> anyhow::Result<Vec<Cell>>;
95
96    /// The unresolved external references the last `run` recorded:
97    /// identifiers (URLs, for html) of documents queries referenced
98    /// (`::href-->`) but could not reach. The engine performs no IO
99    /// — the host may acquire these, remount, and re-run the line.
100    /// Empty by default.
101    fn refs(&self) -> Vec<String> {
102        Vec::new()
103    }
104
105    /// Run against a *freshly re-materialized* source — the `&N!`
106    /// live reading. The default re-runs against the standing arbor
107    /// (an immutable source never drifts); executors that can re-open
108    /// the source override it so a live reading sees current data,
109    /// diverging from the frozen `&N#`.
110    fn run_fresh(&self, query: &str) -> anyhow::Result<Vec<Cell>> {
111        self.run(query)
112    }
113
114    /// Run a query and render its results as exportable markup
115    /// (`md`, `html`, `txt`) — structural through the text-level
116    /// vocabulary for node results, projection lines otherwise.
117    /// Unsupported by default.
118    fn export(&self, _query: &str, _kind: &str) -> anyhow::Result<String> {
119        anyhow::bail!("this session's executor does not support rendered export")
120    }
121
122    /// Tab-completion candidates at a byte cursor. The default
123    /// answers the syntax tier (the stdlib registry needs no
124    /// adapter), so even a daemon-backed session completes
125    /// function names; executors with a live arbor override this
126    /// with the data tier — real children, real annotations.
127    fn complete(&self, text: &str, cursor: usize) -> Vec<quarb::complete::Candidate> {
128        quarb::complete::complete(text, cursor)
129    }
130}
131
132#[cfg(feature = "native")]
133mod daemon;
134#[cfg(feature = "native")]
135pub use daemon::DaemonExecutor;
136
137#[cfg(test)]
138mod mount_spec_tests {
139    use super::MountSpec;
140
141    #[test]
142    fn name_target_splits() {
143        let spec = MountSpec::parse("t=data/titanic.csv");
144        assert_eq!(spec.name.as_deref(), Some("t"));
145        assert_eq!(spec.path.to_str(), Some("data/titanic.csv"));
146        let spec = MountSpec::parse("births-2=x.json");
147        assert_eq!(spec.name.as_deref(), Some("births-2"));
148    }
149
150    #[test]
151    fn bare_paths_stay_bare() {
152        for arg in [
153            "titanic.csv",
154            "git:.",
155            // a left side that isn't a plain mount name is path text
156            "a/b=c.json",
157            "=x.json",
158            "name=",
159        ] {
160            let spec = MountSpec::parse(arg);
161            assert!(spec.name.is_none(), "{arg} should !split");
162            assert_eq!(spec.path.to_str(), Some(arg));
163        }
164    }
165}