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    fn children_named(&self, node: NodeId, name: &str) -> Vec<NodeId> {
129        self.children(node)
130            .into_iter()
131            .filter(|&c| self.name(c).as_deref() == Some(name))
132            .collect()
133    }
134
135    /// The default projection of `node` — bare `::`, adapter-specific
136    /// (a filesystem adapter returns file content).
137    fn default_value(&self, _node: NodeId) -> Option<Value> {
138        None
139    }
140
141    /// Adapter-defined metadata — `::::key` (a filesystem adapter's
142    /// `size`, `modified`, `permissions`, …). `None` if absent.
143    fn metadata(&self, _node: NodeId, _key: &str) -> Option<Value> {
144        None
145    }
146
147    /// Outgoing crosslinks from `node`, as `(label, target)` pairs,
148    /// for `->` navigation (a filesystem adapter's symlinks).
149    fn links(&self, _node: NodeId) -> Vec<(String, NodeId)> {
150        Vec::new()
151    }
152
153    /// Incoming crosslinks to `node`, as `(label, source)` pairs, for
154    /// `<-` navigation. May be expensive (an adapter that does not
155    /// precompute edges must search for referrers).
156    fn backlinks(&self, _node: NodeId) -> Vec<(String, NodeId)> {
157        Vec::new()
158    }
159
160    /// Resolve a cross-reference: `::property~>hint` maps `node`'s
161    /// `property` (a value that references another node) to its target,
162    /// with an optional adapter-specific relation `hint`. A JSON
163    /// adapter resolves a `$ref` JSON Pointer; `None` if unresolvable.
164    fn resolve(&self, _node: NodeId, _property: &str, _hint: Option<&str>) -> Option<NodeId> {
165        None
166    }
167
168    /// A property of the crosslink `source --label--> target` — the
169    /// `$-::prop` read. Adapters whose edges carry data (a property
170    /// graph's relationship properties) override this; `None` if the
171    /// edge is bare or unknown. Where parallel edges share source,
172    /// label, and target, the adapter answers for one of them,
173    /// consistently.
174    fn link_property(
175        &self,
176        _source: NodeId,
177        _label: &str,
178        _target: NodeId,
179        _name: &str,
180    ) -> Option<Value> {
181        None
182    }
183
184    /// The quantifier bound N_max: the depth to which open-ended path
185    /// quantifiers (`+`, `*`, `{m,}`) expand, and the ceiling of any
186    /// explicit `{m,n}` (the effective upper bound is min(n, N_max)).
187    /// An adapter whose natural structures run deep may raise it; the
188    /// CLI overrides it per run (`qua --quantifier-bound`).
189    fn quantifier_bound(&self) -> usize {
190        32
191    }
192
193    /// Whether the `sh(...)` pipeline stage may run external
194    /// commands. False by default — query text stays inert data —
195    /// and enabled per run by the CLI (`qua --allow-shell`) through
196    /// the [`AllowShell`] wrapper.
197    fn allow_shell(&self) -> bool {
198        false
199    }
200
201    /// The invocation instant `now()` denotes (spec: The Temporal
202    /// Fragment, Determinism): one UTC timeline point bound by the
203    /// runner BEFORE evaluation begins — evaluation itself never
204    /// reads a clock. None by default (a library `run` is fully
205    /// deterministic; `now()` reads as null); the CLI binds it at
206    /// startup — pinnable with `qua --now` — through the
207    /// [`WithNow`] wrapper.
208    fn invocation_instant(&self) -> Option<(i64, u32)> {
209        None
210    }
211
212    /// The data provenance of `node` — the `:::source` /
213    /// `:::instant` / `:::dpid` / `:::provenance` core-metadata
214    /// keys. Empty by default: an adapter answers only the
215    /// components its substrate genuinely records (never the
216    /// invocation clock); wrapper adapters fill missing components
217    /// from what they know — the mount its target, a graft its
218    /// outer leaf, a model its derivation — and forward the rest
219    /// inward, so resolution is nearest-ancestor per component.
220    fn provenance(&self, _node: NodeId) -> Provenance {
221        Provenance::default()
222    }
223
224    /// The scale of a unit expression — (factor, canonical SI-base
225    /// expansion) — for the unital reading's criterion text (spec:
226    /// The Quantital Fragment). The default answers from the
227    /// engine's frozen built-in table; a unit-aware adapter (kaiv)
228    /// overrides it to include the mounted document's own custom
229    /// units, so `[::range < '50kellicam']` resolves through the
230    /// document's `.!units` imports.
231    fn unit_scale(&self, expr: &str) -> Option<(f64, String)> {
232        crate::quantity::scale_expr(expr)
233    }
234}
235
236/// An adapter view with the quantifier bound overridden (the CLI's
237/// `--quantifier-bound`); every other method forwards to the wrapped
238/// adapter.
239pub struct QuantifierBound<'a, A: AstAdapter> {
240    pub inner: &'a A,
241    pub bound: usize,
242}
243
244impl<A: AstAdapter> AstAdapter for QuantifierBound<'_, A> {
245    fn root(&self) -> NodeId {
246        self.inner.root()
247    }
248    fn children(&self, node: NodeId) -> Vec<NodeId> {
249        self.inner.children(node)
250    }
251    fn name(&self, node: NodeId) -> Option<String> {
252        self.inner.name(node)
253    }
254    fn parent(&self, node: NodeId) -> Option<NodeId> {
255        self.inner.parent(node)
256    }
257    fn traits(&self, node: NodeId) -> Vec<String> {
258        self.inner.traits(node)
259    }
260    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
261        self.inner.property(node, name)
262    }
263    fn children_named(&self, node: NodeId, name: &str) -> Vec<NodeId> {
264        self.inner.children_named(node, name)
265    }
266    fn default_value(&self, node: NodeId) -> Option<Value> {
267        self.inner.default_value(node)
268    }
269    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
270        self.inner.metadata(node, key)
271    }
272    fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
273        self.inner.links(node)
274    }
275    fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
276        self.inner.backlinks(node)
277    }
278    fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
279        self.inner.resolve(node, property, hint)
280    }
281    fn link_property(
282        &self,
283        source: NodeId,
284        label: &str,
285        target: NodeId,
286        name: &str,
287    ) -> Option<Value> {
288        self.inner.link_property(source, label, target, name)
289    }
290    fn quantifier_bound(&self) -> usize {
291        self.bound
292    }
293    fn allow_shell(&self) -> bool {
294        self.inner.allow_shell()
295    }
296    fn invocation_instant(&self) -> Option<(i64, u32)> {
297        self.inner.invocation_instant()
298    }
299    fn provenance(&self, node: NodeId) -> Provenance {
300        self.inner.provenance(node)
301    }
302    fn unit_scale(&self, expr: &str) -> Option<(f64, String)> {
303        self.inner.unit_scale(expr)
304    }
305}
306
307/// An adapter view with the shell stage enabled (the CLI's
308/// `--allow-shell`); every other method forwards to the wrapped
309/// adapter.
310pub struct AllowShell<'a, A: AstAdapter> {
311    pub inner: &'a A,
312}
313
314impl<A: AstAdapter> AstAdapter for AllowShell<'_, A> {
315    fn root(&self) -> NodeId {
316        self.inner.root()
317    }
318    fn children(&self, node: NodeId) -> Vec<NodeId> {
319        self.inner.children(node)
320    }
321    fn name(&self, node: NodeId) -> Option<String> {
322        self.inner.name(node)
323    }
324    fn parent(&self, node: NodeId) -> Option<NodeId> {
325        self.inner.parent(node)
326    }
327    fn traits(&self, node: NodeId) -> Vec<String> {
328        self.inner.traits(node)
329    }
330    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
331        self.inner.property(node, name)
332    }
333    fn children_named(&self, node: NodeId, name: &str) -> Vec<NodeId> {
334        self.inner.children_named(node, name)
335    }
336    fn default_value(&self, node: NodeId) -> Option<Value> {
337        self.inner.default_value(node)
338    }
339    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
340        self.inner.metadata(node, key)
341    }
342    fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
343        self.inner.links(node)
344    }
345    fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
346        self.inner.backlinks(node)
347    }
348    fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
349        self.inner.resolve(node, property, hint)
350    }
351    fn link_property(
352        &self,
353        source: NodeId,
354        label: &str,
355        target: NodeId,
356        name: &str,
357    ) -> Option<Value> {
358        self.inner.link_property(source, label, target, name)
359    }
360    fn quantifier_bound(&self) -> usize {
361        self.inner.quantifier_bound()
362    }
363    fn allow_shell(&self) -> bool {
364        true
365    }
366    fn invocation_instant(&self) -> Option<(i64, u32)> {
367        self.inner.invocation_instant()
368    }
369    fn provenance(&self, node: NodeId) -> Provenance {
370        self.inner.provenance(node)
371    }
372    fn unit_scale(&self, expr: &str) -> Option<(f64, String)> {
373        self.inner.unit_scale(expr)
374    }
375}
376
377/// An adapter view with the invocation instant bound (the CLI binds
378/// it at startup, `--now` pins it); every other method forwards to
379/// the wrapped adapter.
380pub struct WithNow<'a, A: AstAdapter> {
381    pub inner: &'a A,
382    pub secs: i64,
383    pub nanos: u32,
384}
385
386impl<A: AstAdapter> AstAdapter for WithNow<'_, A> {
387    fn root(&self) -> NodeId {
388        self.inner.root()
389    }
390    fn children(&self, node: NodeId) -> Vec<NodeId> {
391        self.inner.children(node)
392    }
393    fn name(&self, node: NodeId) -> Option<String> {
394        self.inner.name(node)
395    }
396    fn parent(&self, node: NodeId) -> Option<NodeId> {
397        self.inner.parent(node)
398    }
399    fn traits(&self, node: NodeId) -> Vec<String> {
400        self.inner.traits(node)
401    }
402    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
403        self.inner.property(node, name)
404    }
405    fn children_named(&self, node: NodeId, name: &str) -> Vec<NodeId> {
406        self.inner.children_named(node, name)
407    }
408    fn default_value(&self, node: NodeId) -> Option<Value> {
409        self.inner.default_value(node)
410    }
411    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
412        self.inner.metadata(node, key)
413    }
414    fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
415        self.inner.links(node)
416    }
417    fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
418        self.inner.backlinks(node)
419    }
420    fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
421        self.inner.resolve(node, property, hint)
422    }
423    fn link_property(
424        &self,
425        source: NodeId,
426        label: &str,
427        target: NodeId,
428        name: &str,
429    ) -> Option<Value> {
430        self.inner.link_property(source, label, target, name)
431    }
432    fn quantifier_bound(&self) -> usize {
433        self.inner.quantifier_bound()
434    }
435    fn allow_shell(&self) -> bool {
436        self.inner.allow_shell()
437    }
438    fn invocation_instant(&self) -> Option<(i64, u32)> {
439        Some((self.secs, self.nanos))
440    }
441    // Forwarding, not synthesizing: the pinned invocation instant
442    // never becomes a node's `:::instant` — absence is the true
443    // answer for a source that records no time.
444    fn provenance(&self, node: NodeId) -> Provenance {
445        self.inner.provenance(node)
446    }
447    fn unit_scale(&self, expr: &str) -> Option<(f64, String)> {
448        self.inner.unit_scale(expr)
449    }
450}