Skip to main content

quarb/
adapter.rs

1//! The adapter surface: how a data source plugs into the engine.
2//!
3//! The live methods — those the engine currently drives — are
4//! the navigation set ([`root`](AstAdapter::root),
5//! [`children`](AstAdapter::children), [`name`](AstAdapter::name),
6//! [`parent`](AstAdapter::parent)) plus the projection set
7//! ([`traits`](AstAdapter::traits), [`property`](AstAdapter::property),
8//! [`default_value`](AstAdapter::default_value),
9//! [`metadata`](AstAdapter::metadata)). The projection methods have
10//! defaults, so an adapter can implement only what its domain
11//! supports. Crosslink resolution (`-->`) and pattern search (`=>`)
12//! are still planned. See `doc/impl.tex`.
13
14use crate::value::Value;
15
16/// An opaque handle to a node in an arbor.
17///
18/// The engine treats a `NodeId` as an opaque token: it is minted and
19/// interpreted solely by the adapter that produced it. The `u64`
20/// payload is an adapter-private index or key.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct NodeId(pub u64);
23
24/// Per-node data provenance: the three optional components behind
25/// the `:::source` / `:::instant` / `:::dpid` core-metadata keys and
26/// their composite `:::provenance` (`?src@ts#dpid`, kaiv's
27/// spelling). A component is present only where the source genuinely
28/// records it; wrapper adapters layer them ([`or`](Self::or), inner
29/// wins per component).
30#[derive(Debug, Clone, Default, PartialEq)]
31pub struct Provenance {
32    /// Where the datum came from (a URI, a path, a repo).
33    pub source: Option<String>,
34    /// The datum's own instant — `(secs, nanos, offset_min)`, the
35    /// shape `Value::Instant` carries. Never the invocation clock.
36    pub instant: Option<(i64, u32, Option<i16>)>,
37    /// The source-assigned data-point identifier (kaiv `#dpid`).
38    pub dpid: Option<String>,
39}
40
41impl Provenance {
42    /// Component-wise layering: `self` (inner, more specific) wins;
43    /// missing components fill from `outer`.
44    pub fn or(self, outer: Provenance) -> Provenance {
45        Provenance {
46            source: self.source.or(outer.source),
47            instant: self.instant.or(outer.instant),
48            dpid: self.dpid.or(outer.dpid),
49        }
50    }
51
52    pub fn is_empty(&self) -> bool {
53        self.source.is_none() && self.instant.is_none() && self.dpid.is_none()
54    }
55
56    /// The composite canonical text `?src@ts#dpid` — kaiv's
57    /// optionality grammar (`?src`, `?src@ts`, `?@ts#dpid`, …), the
58    /// instant in Quarb's dashed-extended display form. `None` when
59    /// fully empty.
60    pub fn canonical(&self) -> Option<String> {
61        if self.is_empty() {
62            return None;
63        }
64        let mut out = String::from("?");
65        if let Some(src) = &self.source {
66            out.push_str(src);
67        }
68        if let Some((secs, nanos, offset)) = self.instant {
69            out.push('@');
70            out.push_str(&crate::temporal::format_instant(secs, nanos, offset));
71        }
72        if let Some(dpid) = &self.dpid {
73            out.push('#');
74            out.push_str(dpid);
75        }
76        Some(out)
77    }
78}
79
80/// The interface a data source implements to be queried by Quarb.
81///
82/// An adapter maps its native structure onto the arbor model: a tree
83/// backbone whose edges carry *names*. The engine drives navigation
84/// purely through this trait, so the same query language runs over
85/// any adapter.
86pub trait AstAdapter {
87    /// The root node — the initial navigation context.
88    fn root(&self) -> NodeId;
89
90    /// The tree children of `node`, in document order.
91    ///
92    /// Returns an empty vector for a leaf (or an unreadable node).
93    fn children(&self, node: NodeId) -> Vec<NodeId>;
94
95    /// The name of `node` — the label of its incoming tree edge.
96    ///
97    /// `None` when the adapter leaves a node unnamed (typically the
98    /// root; e.g. the filesystem root `/` carries no name).
99    fn name(&self, node: NodeId) -> Option<String>;
100
101    /// The parent of `node`, or `None` for the root.
102    fn parent(&self, _node: NodeId) -> Option<NodeId> {
103        None
104    }
105
106    /// The traits of `node` — its adapter-defined classifications,
107    /// used by `<trait>` navigation filters (e.g. a filesystem
108    /// adapter's `<dir>`, `<code>`, `<image>`).
109    fn traits(&self, _node: NodeId) -> Vec<String> {
110        Vec::new()
111    }
112
113    /// A named property of `node` — `::prop`. `None` if absent.
114    fn property(&self, _node: NodeId, _name: &str) -> Option<Value> {
115        None
116    }
117
118    /// The children of `node` whose edge name is exactly `name` —
119    /// the engine's fast path for name-matcher child hops. The
120    /// default filters [`children`](Self::children); an adapter
121    /// whose containers cannot be enumerated (permission-scoped or
122    /// unbounded remote trees) overrides this with a direct,
123    /// name-addressed lookup. Must be observationally identical to
124    /// the default wherever enumeration works. The adapter owns the
125    /// name test: it may deliberately *alias* — resolve a name to a
126    /// node whose edge name differs (git revision syntax landing on
127    /// a hash-named commit) — and the engine will not re-filter.
128    /// Container-scoped resolution like that stays child-axis only;
129    /// per-node spelling aliases belong in
130    /// [`answers_to`](Self::answers_to), which this default
131    /// consults.
132    fn children_named(&self, node: NodeId, name: &str) -> Vec<NodeId> {
133        self.children(node)
134            .into_iter()
135            .filter(|&c| self.answers_to(c, name))
136            .collect()
137    }
138
139    /// Whether `node` answers to `name` as a literal spelling
140    /// (ruling #30). The default is canonical equality. An adapter
141    /// may declare per-node aliases — a social feed spelling a
142    /// handle `@alice` while the stripped `alice` is the hop name —
143    /// and the engine consults this wherever a *literal* name is
144    /// matched, on every axis, so one override keeps `/@alice` and
145    /// `//@alice` in agreement. `:::name` stays canonical (locators
146    /// and reflection print it; an alias is a way in, never a way
147    /// out), and name patterns (`~(...)`, `*`) test the canonical
148    /// name only.
149    fn answers_to(&self, node: NodeId, name: &str) -> bool {
150        self.name(node).as_deref() == Some(name)
151    }
152
153    /// The default projection of `node` — bare `::`, adapter-specific
154    /// (a filesystem adapter returns file content).
155    fn default_value(&self, _node: NodeId) -> Option<Value> {
156        None
157    }
158
159    /// Adapter-defined metadata — `::::key` (a filesystem adapter's
160    /// `size`, `modified`, `permissions`, …). `None` if absent.
161    fn metadata(&self, _node: NodeId, _key: &str) -> Option<Value> {
162        None
163    }
164
165    /// Metadata keys this adapter also answers at `::` (ruling
166    /// #29), for the given node — per node, because a composite
167    /// adapter (a multi-mount, a graft) answers for whichever
168    /// document owns it. Only an adapter whose property surface is *closed* —
169    /// fixed by the adapter, never grown by document content — may
170    /// declare aliases: a git commit cannot sprout a `short` field,
171    /// where a JSON object can sprout anything. Data always wins:
172    /// the engine consults `property` first and falls through only
173    /// when it answers `None` for that node, so an alias can never
174    /// shadow a document. Core metadata (`:::`) is never aliased —
175    /// `name`, `id` and `index` exist on every node, and aliasing
176    /// them would change what `::name` means wherever a document
177    /// happens to lack that field.
178    fn aliased_metadata(&self, _node: NodeId) -> &'static [&'static str] {
179        &[]
180    }
181
182    /// Outgoing crosslinks from `node`, as `(label, target)` pairs,
183    /// for `->` navigation (a filesystem adapter's symlinks).
184    fn links(&self, _node: NodeId) -> Vec<(String, NodeId)> {
185        Vec::new()
186    }
187
188    /// Incoming crosslinks to `node`, as `(label, source)` pairs, for
189    /// `<-` navigation. May be expensive (an adapter that does not
190    /// precompute edges must search for referrers).
191    fn backlinks(&self, _node: NodeId) -> Vec<(String, NodeId)> {
192        Vec::new()
193    }
194
195    /// Resolve a cross-reference: `::property~>hint` maps `node`'s
196    /// `property` (a value that references another node) to its target,
197    /// with an optional adapter-specific relation `hint`. A JSON
198    /// adapter resolves a `$ref` JSON Pointer; `None` if unresolvable.
199    fn resolve(&self, _node: NodeId, _property: &str, _hint: Option<&str>) -> Option<NodeId> {
200        None
201    }
202
203    /// A property of the crosslink `source --label--> target` — the
204    /// `$-::prop` read. Adapters whose edges carry data (a property
205    /// graph's relationship properties) override this; `None` if the
206    /// edge is bare or unknown. Where parallel edges share source,
207    /// label, and target, the adapter answers for one of them,
208    /// consistently.
209    fn link_property(
210        &self,
211        _source: NodeId,
212        _label: &str,
213        _target: NodeId,
214        _name: &str,
215    ) -> Option<Value> {
216        None
217    }
218
219    /// The quantifier bound N_max: the depth to which open-ended path
220    /// quantifiers (`+`, `*`, `{m,}`) expand, and the ceiling of any
221    /// explicit `{m,n}` (the effective upper bound is min(n, N_max)).
222    /// An adapter whose natural structures run deep may raise it; the
223    /// CLI overrides it per run (`qua --quantifier-bound`).
224    fn quantifier_bound(&self) -> usize {
225        32
226    }
227
228    /// Whether the `sh(...)` pipeline stage may run external
229    /// commands. False by default — query text stays inert data —
230    /// and enabled per run by the CLI (`qua --allow-shell`) through
231    /// the [`AllowShell`] wrapper.
232    fn allow_shell(&self) -> bool {
233        false
234    }
235
236    /// The invocation instant `now()` denotes (spec: The Temporal
237    /// Fragment, Determinism): one UTC timeline point bound by the
238    /// runner BEFORE evaluation begins — evaluation itself never
239    /// reads a clock. None by default (a library `run` is fully
240    /// deterministic; `now()` reads as null); the CLI binds it at
241    /// startup — pinnable with `qua --now` — through the
242    /// [`WithNow`] wrapper.
243    fn invocation_instant(&self) -> Option<(i64, u32)> {
244        None
245    }
246
247    /// The data provenance of `node` — the `:::source` /
248    /// `:::instant` / `:::dpid` / `:::provenance` core-metadata
249    /// keys. Empty by default: an adapter answers only the
250    /// components its substrate genuinely records (never the
251    /// invocation clock); wrapper adapters fill missing components
252    /// from what they know — the mount its target, a graft its
253    /// outer leaf, a model its derivation — and forward the rest
254    /// inward, so resolution is nearest-ancestor per component.
255    fn provenance(&self, _node: NodeId) -> Provenance {
256        Provenance::default()
257    }
258
259    /// The scale of a unit expression — (factor, canonical SI-base
260    /// expansion) — for the unital reading's criterion text (spec:
261    /// The Quantital Fragment). The default answers from the
262    /// engine's frozen built-in table; a unit-aware adapter (kaiv)
263    /// overrides it to include the mounted document's own custom
264    /// units, so `[::range < '50kellicam']` resolves through the
265    /// document's `.!units` imports.
266    fn unit_scale(&self, expr: &str) -> Option<(f64, String)> {
267        crate::quantity::scale_expr(expr)
268    }
269}
270
271/// An adapter view with the quantifier bound overridden (the CLI's
272/// `--quantifier-bound`); every other method forwards to the wrapped
273/// adapter.
274pub struct QuantifierBound<'a, A: AstAdapter> {
275    pub inner: &'a A,
276    pub bound: usize,
277}
278
279impl<A: AstAdapter> AstAdapter for QuantifierBound<'_, A> {
280    fn root(&self) -> NodeId {
281        self.inner.root()
282    }
283    fn children(&self, node: NodeId) -> Vec<NodeId> {
284        self.inner.children(node)
285    }
286    fn name(&self, node: NodeId) -> Option<String> {
287        self.inner.name(node)
288    }
289    fn parent(&self, node: NodeId) -> Option<NodeId> {
290        self.inner.parent(node)
291    }
292    fn traits(&self, node: NodeId) -> Vec<String> {
293        self.inner.traits(node)
294    }
295    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
296        self.inner.property(node, name)
297    }
298    fn children_named(&self, node: NodeId, name: &str) -> Vec<NodeId> {
299        self.inner.children_named(node, name)
300    }
301    fn default_value(&self, node: NodeId) -> Option<Value> {
302        self.inner.default_value(node)
303    }
304    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
305        self.inner.metadata(node, key)
306    }
307    fn aliased_metadata(&self, node: NodeId) -> &'static [&'static str] {
308        self.inner.aliased_metadata(node)
309    }
310    fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
311        self.inner.links(node)
312    }
313    fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
314        self.inner.backlinks(node)
315    }
316    fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
317        self.inner.resolve(node, property, hint)
318    }
319    fn link_property(
320        &self,
321        source: NodeId,
322        label: &str,
323        target: NodeId,
324        name: &str,
325    ) -> Option<Value> {
326        self.inner.link_property(source, label, target, name)
327    }
328    fn quantifier_bound(&self) -> usize {
329        self.bound
330    }
331    fn allow_shell(&self) -> bool {
332        self.inner.allow_shell()
333    }
334    fn invocation_instant(&self) -> Option<(i64, u32)> {
335        self.inner.invocation_instant()
336    }
337    fn provenance(&self, node: NodeId) -> Provenance {
338        self.inner.provenance(node)
339    }
340    fn unit_scale(&self, expr: &str) -> Option<(f64, String)> {
341        self.inner.unit_scale(expr)
342    }
343}
344
345/// An adapter view with the shell stage enabled (the CLI's
346/// `--allow-shell`); every other method forwards to the wrapped
347/// adapter.
348pub struct AllowShell<'a, A: AstAdapter> {
349    pub inner: &'a A,
350}
351
352impl<A: AstAdapter> AstAdapter for AllowShell<'_, A> {
353    fn root(&self) -> NodeId {
354        self.inner.root()
355    }
356    fn children(&self, node: NodeId) -> Vec<NodeId> {
357        self.inner.children(node)
358    }
359    fn name(&self, node: NodeId) -> Option<String> {
360        self.inner.name(node)
361    }
362    fn parent(&self, node: NodeId) -> Option<NodeId> {
363        self.inner.parent(node)
364    }
365    fn traits(&self, node: NodeId) -> Vec<String> {
366        self.inner.traits(node)
367    }
368    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
369        self.inner.property(node, name)
370    }
371    fn children_named(&self, node: NodeId, name: &str) -> Vec<NodeId> {
372        self.inner.children_named(node, name)
373    }
374    fn default_value(&self, node: NodeId) -> Option<Value> {
375        self.inner.default_value(node)
376    }
377    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
378        self.inner.metadata(node, key)
379    }
380    fn aliased_metadata(&self, node: NodeId) -> &'static [&'static str] {
381        self.inner.aliased_metadata(node)
382    }
383    fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
384        self.inner.links(node)
385    }
386    fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
387        self.inner.backlinks(node)
388    }
389    fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
390        self.inner.resolve(node, property, hint)
391    }
392    fn link_property(
393        &self,
394        source: NodeId,
395        label: &str,
396        target: NodeId,
397        name: &str,
398    ) -> Option<Value> {
399        self.inner.link_property(source, label, target, name)
400    }
401    fn quantifier_bound(&self) -> usize {
402        self.inner.quantifier_bound()
403    }
404    fn allow_shell(&self) -> bool {
405        true
406    }
407    fn invocation_instant(&self) -> Option<(i64, u32)> {
408        self.inner.invocation_instant()
409    }
410    fn provenance(&self, node: NodeId) -> Provenance {
411        self.inner.provenance(node)
412    }
413    fn unit_scale(&self, expr: &str) -> Option<(f64, String)> {
414        self.inner.unit_scale(expr)
415    }
416}
417
418/// An adapter view with the invocation instant bound (the CLI binds
419/// it at startup, `--now` pins it); every other method forwards to
420/// the wrapped adapter.
421pub struct WithNow<'a, A: AstAdapter> {
422    pub inner: &'a A,
423    pub secs: i64,
424    pub nanos: u32,
425}
426
427impl<A: AstAdapter> AstAdapter for WithNow<'_, A> {
428    fn root(&self) -> NodeId {
429        self.inner.root()
430    }
431    fn children(&self, node: NodeId) -> Vec<NodeId> {
432        self.inner.children(node)
433    }
434    fn name(&self, node: NodeId) -> Option<String> {
435        self.inner.name(node)
436    }
437    fn parent(&self, node: NodeId) -> Option<NodeId> {
438        self.inner.parent(node)
439    }
440    fn traits(&self, node: NodeId) -> Vec<String> {
441        self.inner.traits(node)
442    }
443    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
444        self.inner.property(node, name)
445    }
446    fn children_named(&self, node: NodeId, name: &str) -> Vec<NodeId> {
447        self.inner.children_named(node, name)
448    }
449    fn default_value(&self, node: NodeId) -> Option<Value> {
450        self.inner.default_value(node)
451    }
452    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
453        self.inner.metadata(node, key)
454    }
455    fn aliased_metadata(&self, node: NodeId) -> &'static [&'static str] {
456        self.inner.aliased_metadata(node)
457    }
458    fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
459        self.inner.links(node)
460    }
461    fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
462        self.inner.backlinks(node)
463    }
464    fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
465        self.inner.resolve(node, property, hint)
466    }
467    fn link_property(
468        &self,
469        source: NodeId,
470        label: &str,
471        target: NodeId,
472        name: &str,
473    ) -> Option<Value> {
474        self.inner.link_property(source, label, target, name)
475    }
476    fn quantifier_bound(&self) -> usize {
477        self.inner.quantifier_bound()
478    }
479    fn allow_shell(&self) -> bool {
480        self.inner.allow_shell()
481    }
482    fn invocation_instant(&self) -> Option<(i64, u32)> {
483        Some((self.secs, self.nanos))
484    }
485    // Forwarding, not synthesizing: the pinned invocation instant
486    // never becomes a node's `:::instant` — absence is the true
487    // answer for a source that records no time.
488    fn provenance(&self, node: NodeId) -> Provenance {
489        self.inner.provenance(node)
490    }
491    fn unit_scale(&self, expr: &str) -> Option<(f64, String)> {
492        self.inner.unit_scale(expr)
493    }
494}