Skip to main content

quarb_compose/
lib.rs

1//! Adapter composition: descend *through* a leaf into its content.
2//!
3//! A filesystem's `.json` file, an archive's `config.xml` entry, a
4//! message's HTML part — trees of files are full of leaves whose
5//! *content* is itself a tree in another notation. Composition
6//! grafts that inner tree onto the outer one: wrap any adapter in
7//! a [`ComposeAdapter`] and a leaf whose name or content says
8//! "parse me" gains the parsed arbor as its children:
9//!
10//! ```text
11//! /data/store.json/books/*[/price:: < 12]/title::
12//!       └ outer (fs) ┘└──── inner (json) ────┘
13//! ```
14//!
15//! The graft happens on first touch, lazily: a leaf is only read
16//! and parsed when navigation actually enters it. Detection is by
17//! extension (`.json`, `.xml`, `.html`/`.htm`/`.xhtml`/`.svg`,
18//! `.csv`/`.tsv`), else by sniffing the content's first
19//! character; a parse failure simply leaves the leaf a leaf.
20//! Archive leaves (`.zip`, `.tar`, `.tar.gz`) are binary, so they
21//! graft by *path* rather than content — available when the outer
22//! substrate has real paths (see
23//! [`ComposeAdapter::with_source_paths`]) — and the grafted
24//! archive composes in turn, so one path runs filesystem → tar →
25//! JSON without a seam. The
26//! inner root is *identified with* the outer leaf node — the leaf
27//! itself answers the inner root's children — so there is no
28//! phantom node between the file and its content.
29//!
30//! Locators show the boundary with a bang, jar-URL style:
31//! `store.json!/books/1`.
32
33use quarb::{AstAdapter, NodeId, Value};
34use std::cell::RefCell;
35use std::collections::HashMap;
36use std::path::PathBuf;
37
38/// A parsed inner arbor.
39enum Inner {
40    Json(quarb_json::JsonAdapter),
41    Xml(quarb_xml::XmlAdapter),
42    Html(quarb_html::HtmlAdapter),
43    Text(quarb_text::TextModel),
44    Csv(quarb_csv::CsvAdapter),
45    Syntax(quarb_tree_sitter::TreeSitterAdapter),
46    Code(quarb_code::CodeModel),
47    /// A grafted archive, itself composed so its own parseable
48    /// entries graft in turn.
49    Archive(Box<ComposeAdapter<quarb_archive::ArchiveAdapter>>),
50}
51
52impl Inner {
53    fn adapter(&self) -> &dyn AstAdapter {
54        match self {
55            Inner::Json(a) => a,
56            Inner::Xml(a) => a,
57            Inner::Html(a) => a,
58            Inner::Text(a) => a,
59            Inner::Csv(a) => a,
60            Inner::Syntax(a) => a,
61            Inner::Code(a) => a,
62            Inner::Archive(a) => &**a,
63        }
64    }
65
66    fn locator(&self, node: NodeId) -> String {
67        match self {
68            Inner::Json(a) => a.pointer(node),
69            Inner::Xml(a) => a.locator(node),
70            Inner::Html(a) => a.locator(node),
71            Inner::Text(a) => a.locator(node),
72            Inner::Csv(a) => a.locator(node),
73            Inner::Syntax(a) => a.locator(node),
74            Inner::Code(a) => a.locator(node),
75            Inner::Archive(a) => a.locator(node, |o| a.outer().locator(o)),
76        }
77    }
78}
79
80/// The level source-file leaves graft at: the syntax level (the
81/// literal parse — the default) or the code level (identifiers
82/// as names). Chosen per mount by the `code:` target prefix, and
83/// inherited by nested composes (an archive inside a `code:`
84/// mount grafts its source entries at the code level too) — the
85/// filesystem-into-declarations namespace has no seam.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
87pub enum SourceGraft {
88    #[default]
89    Syntax,
90    Code,
91}
92
93/// Names [`quarb_archive::ArchiveAdapter::open`] can take: the
94/// zip family, tar, and gzip (`.gz` alone included — the open
95/// checks magic bytes, and a failure leaves the leaf a leaf).
96fn archive_name(name: &str) -> bool {
97    let ext = name.rsplit('.').next().unwrap_or("").to_ascii_lowercase();
98    matches!(ext.as_str(), "zip" | "tar" | "tgz" | "gz")
99}
100
101/// Parse `content` by `name`'s extension, else by sniffing.
102fn parse_inner(name: &str, content: &str, graft: SourceGraft) -> Option<Inner> {
103    let ext = name.rsplit('.').next().unwrap_or("").to_ascii_lowercase();
104    match ext.as_str() {
105        "json" => {
106            return quarb_json::JsonAdapter::parse(content)
107                .ok()
108                .map(Inner::Json);
109        }
110        "jsonl" | "ndjson" => {
111            return quarb_json::JsonAdapter::parse_lines(content)
112                .ok()
113                .map(Inner::Json);
114        }
115        "xml" | "svg" | "xhtml" => {
116            return quarb_xml::XmlAdapter::parse(content).ok().map(Inner::Xml);
117        }
118        "html" | "htm" => return Some(Inner::Html(quarb_html::HtmlAdapter::parse(content))),
119        "yaml" | "yml" => return quarb_yaml::parse(content).ok().map(Inner::Json),
120        "toml" => return quarb_toml::parse(content).ok().map(Inner::Json),
121        "md" | "markdown" => return Some(Inner::Html(quarb_markdown::parse(content))),
122        // Plain text grafts at the text level (blank-line
123        // paragraphs); html/md keep their DOM-level graft.
124        "txt" => return Some(Inner::Text(quarb_text::TextModel::parse_plain(content))),
125        "csv" => return quarb_csv::CsvAdapter::parse(content).ok().map(Inner::Csv),
126        "tsv" => {
127            return quarb_csv::CsvAdapter::parse_with_delimiter(content, b'\t')
128                .ok()
129                .map(Inner::Csv);
130        }
131        ext if quarb_tree_sitter::supported(ext) => {
132            return match graft {
133                SourceGraft::Syntax => quarb_tree_sitter::TreeSitterAdapter::parse(content, ext)
134                    .ok()
135                    .map(Inner::Syntax),
136                SourceGraft::Code => {
137                    quarb_code::CodeModel::parse(content, ext).ok().map(Inner::Code)
138                }
139            };
140        }
141        _ => {}
142    }
143    // Content sniff for extensionless names.
144    let t = content.trim_start();
145    if t.starts_with('{') || t.starts_with('[') {
146        return quarb_json::JsonAdapter::parse(content)
147            .ok()
148            .map(Inner::Json);
149    }
150    if t.starts_with("<?xml") {
151        return quarb_xml::XmlAdapter::parse(content).ok().map(Inner::Xml);
152    }
153    None
154}
155
156/// One graft: an inner arbor mounted at an outer leaf.
157struct Graft {
158    outer: NodeId,
159    inner: Inner,
160}
161
162/// Grafted node ids carry this tag bit; the rest indexes the intern
163/// table. The bit sits at the top of the 56-bit id window that
164/// `MountAdapter` leaves an inner adapter — it reserves the high byte
165/// (bits 56–63) for the mount index — so a grafted id survives being
166/// packed into a mount and still round-trips. It must stay below bit
167/// 56 for that reason; the top bit (1 << 63) collided with the mount
168/// byte. Outer adapters use small sequential indices, far below this
169/// bit, so a set bit 55 unambiguously marks a graft.
170const GRAFT_BIT: u64 = 1 << 55;
171
172/// Any adapter, with parseable leaf content grafted as subtrees.
173pub struct ComposeAdapter<A: AstAdapter> {
174    outer: A,
175    grafts: RefCell<Vec<Graft>>,
176    /// Outer leaf → its graft index (`None`: probed, not
177    /// parseable).
178    probed: RefCell<HashMap<NodeId, Option<usize>>>,
179    /// (graft, inner id) → interned composite id, and the reverse.
180    interned: RefCell<HashMap<(usize, NodeId), NodeId>>,
181    reverse: RefCell<Vec<(usize, NodeId)>>,
182    /// Maps an outer leaf to a filesystem path, when the outer
183    /// substrate has one. Enables archive grafts, which are
184    /// binary and open by path rather than parsing from text.
185    source_path: Option<fn(&A, NodeId) -> Option<PathBuf>>,
186    /// The level source-file leaves graft at.
187    source_graft: SourceGraft,
188}
189
190impl<A: AstAdapter> ComposeAdapter<A> {
191    pub fn new(outer: A) -> Self {
192        ComposeAdapter {
193            outer,
194            grafts: RefCell::new(Vec::new()),
195            probed: RefCell::new(HashMap::new()),
196            interned: RefCell::new(HashMap::new()),
197            reverse: RefCell::new(Vec::new()),
198            source_path: None,
199            source_graft: SourceGraft::default(),
200        }
201    }
202
203    /// Choose the level source-file leaves graft at
204    /// (builder-style; the default is the syntax level).
205    pub fn with_source_graft(mut self, graft: SourceGraft) -> Self {
206        self.source_graft = graft;
207        self
208    }
209
210    /// Like [`new`](Self::new), with a hook mapping outer leaves
211    /// to filesystem paths, so archive leaves (`.zip`,
212    /// `.tar[.gz]`) graft too. The grafted archive composes in
213    /// turn: its own parseable entries graft, and one path walks
214    /// filesystem → archive → document.
215    pub fn with_source_paths(outer: A, source_path: fn(&A, NodeId) -> Option<PathBuf>) -> Self {
216        ComposeAdapter {
217            source_path: Some(source_path),
218            ..Self::new(outer)
219        }
220    }
221
222    /// The wrapped adapter (for outer-specific calls).
223    pub fn outer(&self) -> &A {
224        &self.outer
225    }
226
227    /// A combined locator: `outer_locator(leaf)!inner-path` for
228    /// grafted nodes.
229    pub fn locator(&self, node: NodeId, outer_locator: impl Fn(NodeId) -> String) -> String {
230        match self.split(node) {
231            None => outer_locator(node),
232            Some((g, inner)) => {
233                let grafts = self.grafts.borrow();
234                let graft = &grafts[g];
235                format!(
236                    "{}!{}",
237                    outer_locator(graft.outer),
238                    graft.inner.locator(inner)
239                )
240            }
241        }
242    }
243
244    /// Decode a composite id.
245    fn split(&self, node: NodeId) -> Option<(usize, NodeId)> {
246        if node.0 & GRAFT_BIT == 0 {
247            return None;
248        }
249        self.reverse
250            .borrow()
251            .get((node.0 & !GRAFT_BIT) as usize)
252            .copied()
253    }
254
255    fn intern(&self, graft: usize, inner: NodeId) -> NodeId {
256        if let Some(&id) = self.interned.borrow().get(&(graft, inner)) {
257            return id;
258        }
259        let mut rev = self.reverse.borrow_mut();
260        let id = NodeId(GRAFT_BIT | rev.len() as u64);
261        rev.push((graft, inner));
262        self.interned.borrow_mut().insert((graft, inner), id);
263        id
264    }
265
266    /// The graft at an outer leaf, probing (read + parse) on first
267    /// touch. Only childless outer nodes with text content are
268    /// candidates.
269    fn graft_at(&self, node: NodeId) -> Option<usize> {
270        if let Some(&g) = self.probed.borrow().get(&node) {
271            return g;
272        }
273        let g = (|| {
274            if !self.outer.children(node).is_empty() {
275                return None;
276            }
277            let name = self.outer.name(node)?;
278            if archive_name(&name)
279                && let Some(path_of) = self.source_path
280                && let Some(path) = path_of(&self.outer, node)
281                && let Ok(a) = quarb_archive::ArchiveAdapter::open(&path)
282            {
283                let inner = Inner::Archive(Box::new(
284                    ComposeAdapter::new(a).with_source_graft(self.source_graft),
285                ));
286                let mut grafts = self.grafts.borrow_mut();
287                grafts.push(Graft { outer: node, inner });
288                return Some(grafts.len() - 1);
289            }
290            let content = match self.outer.default_value(node)? {
291                Value::Str(s) => s,
292                _ => return None,
293            };
294            let inner = parse_inner(&name, &content, self.source_graft)?;
295            let mut grafts = self.grafts.borrow_mut();
296            grafts.push(Graft { outer: node, inner });
297            Some(grafts.len() - 1)
298        })();
299        self.probed.borrow_mut().insert(node, g);
300        g
301    }
302
303    /// Map an inner node up: the inner root becomes the outer
304    /// leaf.
305    fn wrap(&self, graft: usize, inner: NodeId) -> NodeId {
306        let grafts = self.grafts.borrow();
307        if inner == grafts[graft].inner.adapter().root() {
308            grafts[graft].outer
309        } else {
310            drop(grafts);
311            self.intern(graft, inner)
312        }
313    }
314}
315
316impl<A: AstAdapter> AstAdapter for ComposeAdapter<A> {
317    fn root(&self) -> NodeId {
318        self.outer.root()
319    }
320
321    fn children(&self, node: NodeId) -> Vec<NodeId> {
322        match self.split(node) {
323            Some((g, inner)) => {
324                let ids: Vec<NodeId> = {
325                    let grafts = self.grafts.borrow();
326                    grafts[g].inner.adapter().children(inner)
327                };
328                ids.into_iter().map(|c| self.wrap(g, c)).collect()
329            }
330            None => {
331                let outer = self.outer.children(node);
332                if !outer.is_empty() {
333                    return outer;
334                }
335                match self.graft_at(node) {
336                    Some(g) => {
337                        let ids: Vec<NodeId> = {
338                            let grafts = self.grafts.borrow();
339                            let a = grafts[g].inner.adapter();
340                            a.children(a.root())
341                        };
342                        ids.into_iter().map(|c| self.wrap(g, c)).collect()
343                    }
344                    None => Vec::new(),
345                }
346            }
347        }
348    }
349
350    fn name(&self, node: NodeId) -> Option<String> {
351        match self.split(node) {
352            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().name(inner),
353            None => self.outer.name(node),
354        }
355    }
356
357    fn parent(&self, node: NodeId) -> Option<NodeId> {
358        match self.split(node) {
359            Some((g, inner)) => {
360                let p = self.grafts.borrow()[g].inner.adapter().parent(inner)?;
361                Some(self.wrap(g, p))
362            }
363            None => self.outer.parent(node),
364        }
365    }
366
367    fn traits(&self, node: NodeId) -> Vec<String> {
368        match self.split(node) {
369            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().traits(inner),
370            None => self.outer.traits(node),
371        }
372    }
373
374    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
375        match self.split(node) {
376            Some((g, inner)) => self.grafts.borrow()[g]
377                .inner
378                .adapter()
379                .property(inner, name),
380            // The graft root IS the outer leaf, so a property the
381            // outer can't answer falls through to the grafted
382            // document's root — `::key` dual exposure works *at*
383            // the graft, not only one level in.
384            None => self.outer.property(node, name).or_else(|| {
385                let g = self.graft_at(node)?;
386                let grafts = self.grafts.borrow();
387                let a = grafts[g].inner.adapter();
388                a.property(a.root(), name)
389            }),
390        }
391    }
392
393    fn default_value(&self, node: NodeId) -> Option<Value> {
394        match self.split(node) {
395            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().default_value(inner),
396            None => self.outer.default_value(node),
397        }
398    }
399
400    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
401        match self.split(node) {
402            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().metadata(inner, key),
403            None => self.outer.metadata(node, key),
404        }
405    }
406
407    /// A graft's own aliases are not merged: a node either belongs
408    /// to the outer document or to a graft, and the alias list is
409    /// consulted only after the owning adapter's `property` misses.
410    fn aliased_metadata(&self, node: NodeId) -> &'static [&'static str] {
411        match self.split(node) {
412            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().aliased_metadata(inner),
413            None => self.outer.aliased_metadata(node),
414        }
415    }
416
417    /// A graft is a provenance layer: the grafted document's own
418    /// components win, and the outer *leaf* it was parsed from fills
419    /// the rest (with an fs outer: the file's path and mtime). The
420    /// jar-style composite locator is deliberately not the source —
421    /// `?src` names a document, and the intra-document address is
422    /// dpid's job.
423    fn provenance(&self, node: NodeId) -> quarb::Provenance {
424        match self.split(node) {
425            Some((g, inner)) => {
426                // Clone what we need out of the borrow before calling
427                // into `self.outer` (which may re-enter the graft
428                // tables).
429                let (inner_prov, outer_leaf) = {
430                    let grafts = self.grafts.borrow();
431                    (
432                        grafts[g].inner.adapter().provenance(inner),
433                        grafts[g].outer,
434                    )
435                };
436                inner_prov.or(self.outer.provenance(outer_leaf))
437            }
438            None => self.outer.provenance(node),
439        }
440    }
441
442    fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
443        match self.split(node) {
444            Some((g, inner)) => {
445                let t = self.grafts.borrow()[g]
446                    .inner
447                    .adapter()
448                    .resolve(inner, property, hint)?;
449                Some(self.wrap(g, t))
450            }
451            None => self.outer.resolve(node, property, hint),
452        }
453    }
454
455    fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
456        match self.split(node) {
457            Some((g, inner)) => {
458                let ls: Vec<(String, NodeId)> = {
459                    let grafts = self.grafts.borrow();
460                    grafts[g].inner.adapter().links(inner)
461                };
462                ls.into_iter().map(|(l, n)| (l, self.wrap(g, n))).collect()
463            }
464            None => self.outer.links(node),
465        }
466    }
467
468    fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
469        match self.split(node) {
470            Some((g, inner)) => {
471                let ls: Vec<(String, NodeId)> = {
472                    let grafts = self.grafts.borrow();
473                    grafts[g].inner.adapter().backlinks(inner)
474                };
475                ls.into_iter().map(|(l, n)| (l, self.wrap(g, n))).collect()
476            }
477            None => self.outer.backlinks(node),
478        }
479    }
480
481    fn link_property(
482        &self,
483        source: NodeId,
484        label: &str,
485        target: NodeId,
486        name: &str,
487    ) -> Option<Value> {
488        // An edge lives on one side of the graft boundary: both
489        // endpoints are inner (same graft) or both outer.
490        match (self.split(source), self.split(target)) {
491            (Some((g, src)), Some((gt, tgt))) if g == gt => self.grafts.borrow()[g]
492                .inner
493                .adapter()
494                .link_property(src, label, tgt, name),
495            (None, None) => self.outer.link_property(source, label, target, name),
496            _ => None,
497        }
498    }
499}