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}