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    Csv(quarb_csv::CsvAdapter),
44    Code(quarb_code::CodeAdapter),
45    /// A grafted archive, itself composed so its own parseable
46    /// entries graft in turn.
47    Archive(Box<ComposeAdapter<quarb_archive::ArchiveAdapter>>),
48}
49
50impl Inner {
51    fn adapter(&self) -> &dyn AstAdapter {
52        match self {
53            Inner::Json(a) => a,
54            Inner::Xml(a) => a,
55            Inner::Html(a) => a,
56            Inner::Csv(a) => a,
57            Inner::Code(a) => a,
58            Inner::Archive(a) => &**a,
59        }
60    }
61
62    fn locator(&self, node: NodeId) -> String {
63        match self {
64            Inner::Json(a) => a.pointer(node),
65            Inner::Xml(a) => a.locator(node),
66            Inner::Html(a) => a.locator(node),
67            Inner::Csv(a) => a.locator(node),
68            Inner::Code(a) => a.locator(node),
69            Inner::Archive(a) => a.locator(node, |o| a.outer().locator(o)),
70        }
71    }
72}
73
74/// Names [`quarb_archive::ArchiveAdapter::open`] can take: the
75/// zip family, tar, and gzip (`.gz` alone included — the open
76/// checks magic bytes, and a failure leaves the leaf a leaf).
77fn archive_name(name: &str) -> bool {
78    let ext = name.rsplit('.').next().unwrap_or("").to_ascii_lowercase();
79    matches!(ext.as_str(), "zip" | "tar" | "tgz" | "gz")
80}
81
82/// Parse `content` by `name`'s extension, else by sniffing.
83fn parse_inner(name: &str, content: &str) -> Option<Inner> {
84    let ext = name.rsplit('.').next().unwrap_or("").to_ascii_lowercase();
85    match ext.as_str() {
86        "json" => {
87            return quarb_json::JsonAdapter::parse(content)
88                .ok()
89                .map(Inner::Json);
90        }
91        "jsonl" | "ndjson" => {
92            return quarb_json::JsonAdapter::parse_lines(content)
93                .ok()
94                .map(Inner::Json);
95        }
96        "xml" | "svg" | "xhtml" => {
97            return quarb_xml::XmlAdapter::parse(content).ok().map(Inner::Xml);
98        }
99        "html" | "htm" => return Some(Inner::Html(quarb_html::HtmlAdapter::parse(content))),
100        "yaml" | "yml" => return quarb_yaml::parse(content).ok().map(Inner::Json),
101        "toml" => return quarb_toml::parse(content).ok().map(Inner::Json),
102        "md" | "markdown" => return Some(Inner::Html(quarb_markdown::parse(content))),
103        "csv" => return quarb_csv::CsvAdapter::parse(content).ok().map(Inner::Csv),
104        "tsv" => {
105            return quarb_csv::CsvAdapter::parse_with_delimiter(content, b'\t')
106                .ok()
107                .map(Inner::Csv);
108        }
109        ext if quarb_code::supported(ext) => {
110            return quarb_code::CodeAdapter::parse(content, ext)
111                .ok()
112                .map(Inner::Code);
113        }
114        _ => {}
115    }
116    // Content sniff for extensionless names.
117    let t = content.trim_start();
118    if t.starts_with('{') || t.starts_with('[') {
119        return quarb_json::JsonAdapter::parse(content)
120            .ok()
121            .map(Inner::Json);
122    }
123    if t.starts_with("<?xml") {
124        return quarb_xml::XmlAdapter::parse(content).ok().map(Inner::Xml);
125    }
126    None
127}
128
129/// One graft: an inner arbor mounted at an outer leaf.
130struct Graft {
131    outer: NodeId,
132    inner: Inner,
133}
134
135/// Grafted node ids carry this tag bit; the rest indexes the intern
136/// table. The bit sits at the top of the 56-bit id window that
137/// `MountAdapter` leaves an inner adapter — it reserves the high byte
138/// (bits 56–63) for the mount index — so a grafted id survives being
139/// packed into a mount and still round-trips. It must stay below bit
140/// 56 for that reason; the top bit (1 << 63) collided with the mount
141/// byte. Outer adapters use small sequential indices, far below this
142/// bit, so a set bit 55 unambiguously marks a graft.
143const GRAFT_BIT: u64 = 1 << 55;
144
145/// Any adapter, with parseable leaf content grafted as subtrees.
146pub struct ComposeAdapter<A: AstAdapter> {
147    outer: A,
148    grafts: RefCell<Vec<Graft>>,
149    /// Outer leaf → its graft index (`None`: probed, not
150    /// parseable).
151    probed: RefCell<HashMap<NodeId, Option<usize>>>,
152    /// (graft, inner id) → interned composite id, and the reverse.
153    interned: RefCell<HashMap<(usize, NodeId), NodeId>>,
154    reverse: RefCell<Vec<(usize, NodeId)>>,
155    /// Maps an outer leaf to a filesystem path, when the outer
156    /// substrate has one. Enables archive grafts, which are
157    /// binary and open by path rather than parsing from text.
158    source_path: Option<fn(&A, NodeId) -> Option<PathBuf>>,
159}
160
161impl<A: AstAdapter> ComposeAdapter<A> {
162    pub fn new(outer: A) -> Self {
163        ComposeAdapter {
164            outer,
165            grafts: RefCell::new(Vec::new()),
166            probed: RefCell::new(HashMap::new()),
167            interned: RefCell::new(HashMap::new()),
168            reverse: RefCell::new(Vec::new()),
169            source_path: None,
170        }
171    }
172
173    /// Like [`new`](Self::new), with a hook mapping outer leaves
174    /// to filesystem paths, so archive leaves (`.zip`,
175    /// `.tar[.gz]`) graft too. The grafted archive composes in
176    /// turn: its own parseable entries graft, and one path walks
177    /// filesystem → archive → document.
178    pub fn with_source_paths(outer: A, source_path: fn(&A, NodeId) -> Option<PathBuf>) -> Self {
179        ComposeAdapter {
180            source_path: Some(source_path),
181            ..Self::new(outer)
182        }
183    }
184
185    /// The wrapped adapter (for outer-specific calls).
186    pub fn outer(&self) -> &A {
187        &self.outer
188    }
189
190    /// A combined locator: `outer_locator(leaf)!inner-path` for
191    /// grafted nodes.
192    pub fn locator(&self, node: NodeId, outer_locator: impl Fn(NodeId) -> String) -> String {
193        match self.split(node) {
194            None => outer_locator(node),
195            Some((g, inner)) => {
196                let grafts = self.grafts.borrow();
197                let graft = &grafts[g];
198                format!(
199                    "{}!{}",
200                    outer_locator(graft.outer),
201                    graft.inner.locator(inner)
202                )
203            }
204        }
205    }
206
207    /// Decode a composite id.
208    fn split(&self, node: NodeId) -> Option<(usize, NodeId)> {
209        if node.0 & GRAFT_BIT == 0 {
210            return None;
211        }
212        self.reverse
213            .borrow()
214            .get((node.0 & !GRAFT_BIT) as usize)
215            .copied()
216    }
217
218    fn intern(&self, graft: usize, inner: NodeId) -> NodeId {
219        if let Some(&id) = self.interned.borrow().get(&(graft, inner)) {
220            return id;
221        }
222        let mut rev = self.reverse.borrow_mut();
223        let id = NodeId(GRAFT_BIT | rev.len() as u64);
224        rev.push((graft, inner));
225        self.interned.borrow_mut().insert((graft, inner), id);
226        id
227    }
228
229    /// The graft at an outer leaf, probing (read + parse) on first
230    /// touch. Only childless outer nodes with text content are
231    /// candidates.
232    fn graft_at(&self, node: NodeId) -> Option<usize> {
233        if let Some(&g) = self.probed.borrow().get(&node) {
234            return g;
235        }
236        let g = (|| {
237            if !self.outer.children(node).is_empty() {
238                return None;
239            }
240            let name = self.outer.name(node)?;
241            if archive_name(&name)
242                && let Some(path_of) = self.source_path
243                && let Some(path) = path_of(&self.outer, node)
244                && let Ok(a) = quarb_archive::ArchiveAdapter::open(&path)
245            {
246                let inner = Inner::Archive(Box::new(ComposeAdapter::new(a)));
247                let mut grafts = self.grafts.borrow_mut();
248                grafts.push(Graft { outer: node, inner });
249                return Some(grafts.len() - 1);
250            }
251            let content = match self.outer.default_value(node)? {
252                Value::Str(s) => s,
253                _ => return None,
254            };
255            let inner = parse_inner(&name, &content)?;
256            let mut grafts = self.grafts.borrow_mut();
257            grafts.push(Graft { outer: node, inner });
258            Some(grafts.len() - 1)
259        })();
260        self.probed.borrow_mut().insert(node, g);
261        g
262    }
263
264    /// Map an inner node up: the inner root becomes the outer
265    /// leaf.
266    fn wrap(&self, graft: usize, inner: NodeId) -> NodeId {
267        let grafts = self.grafts.borrow();
268        if inner == grafts[graft].inner.adapter().root() {
269            grafts[graft].outer
270        } else {
271            drop(grafts);
272            self.intern(graft, inner)
273        }
274    }
275}
276
277impl<A: AstAdapter> AstAdapter for ComposeAdapter<A> {
278    fn root(&self) -> NodeId {
279        self.outer.root()
280    }
281
282    fn children(&self, node: NodeId) -> Vec<NodeId> {
283        match self.split(node) {
284            Some((g, inner)) => {
285                let ids: Vec<NodeId> = {
286                    let grafts = self.grafts.borrow();
287                    grafts[g].inner.adapter().children(inner)
288                };
289                ids.into_iter().map(|c| self.wrap(g, c)).collect()
290            }
291            None => {
292                let outer = self.outer.children(node);
293                if !outer.is_empty() {
294                    return outer;
295                }
296                match self.graft_at(node) {
297                    Some(g) => {
298                        let ids: Vec<NodeId> = {
299                            let grafts = self.grafts.borrow();
300                            let a = grafts[g].inner.adapter();
301                            a.children(a.root())
302                        };
303                        ids.into_iter().map(|c| self.wrap(g, c)).collect()
304                    }
305                    None => Vec::new(),
306                }
307            }
308        }
309    }
310
311    fn name(&self, node: NodeId) -> Option<String> {
312        match self.split(node) {
313            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().name(inner),
314            None => self.outer.name(node),
315        }
316    }
317
318    fn parent(&self, node: NodeId) -> Option<NodeId> {
319        match self.split(node) {
320            Some((g, inner)) => {
321                let p = self.grafts.borrow()[g].inner.adapter().parent(inner)?;
322                Some(self.wrap(g, p))
323            }
324            None => self.outer.parent(node),
325        }
326    }
327
328    fn traits(&self, node: NodeId) -> Vec<String> {
329        match self.split(node) {
330            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().traits(inner),
331            None => self.outer.traits(node),
332        }
333    }
334
335    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
336        match self.split(node) {
337            Some((g, inner)) => self.grafts.borrow()[g]
338                .inner
339                .adapter()
340                .property(inner, name),
341            None => self.outer.property(node, name),
342        }
343    }
344
345    fn default_value(&self, node: NodeId) -> Option<Value> {
346        match self.split(node) {
347            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().default_value(inner),
348            None => self.outer.default_value(node),
349        }
350    }
351
352    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
353        match self.split(node) {
354            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().metadata(inner, key),
355            None => self.outer.metadata(node, key),
356        }
357    }
358
359    fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
360        match self.split(node) {
361            Some((g, inner)) => {
362                let t = self.grafts.borrow()[g]
363                    .inner
364                    .adapter()
365                    .resolve(inner, property, hint)?;
366                Some(self.wrap(g, t))
367            }
368            None => self.outer.resolve(node, property, hint),
369        }
370    }
371
372    fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
373        match self.split(node) {
374            Some((g, inner)) => {
375                let ls: Vec<(String, NodeId)> = {
376                    let grafts = self.grafts.borrow();
377                    grafts[g].inner.adapter().links(inner)
378                };
379                ls.into_iter().map(|(l, n)| (l, self.wrap(g, n))).collect()
380            }
381            None => self.outer.links(node),
382        }
383    }
384
385    fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
386        match self.split(node) {
387            Some((g, inner)) => {
388                let ls: Vec<(String, NodeId)> = {
389                    let grafts = self.grafts.borrow();
390                    grafts[g].inner.adapter().backlinks(inner)
391                };
392                ls.into_iter().map(|(l, n)| (l, self.wrap(g, n))).collect()
393            }
394            None => self.outer.backlinks(node),
395        }
396    }
397
398    fn link_property(
399        &self,
400        source: NodeId,
401        label: &str,
402        target: NodeId,
403        name: &str,
404    ) -> Option<Value> {
405        // An edge lives on one side of the graft boundary: both
406        // endpoints are inner (same graft) or both outer.
407        match (self.split(source), self.split(target)) {
408            (Some((g, src)), Some((gt, tgt))) if g == gt => self.grafts.borrow()[g]
409                .inner
410                .adapter()
411                .link_property(src, label, tgt, name),
412            (None, None) => self.outer.link_property(source, label, target, name),
413            _ => None,
414        }
415    }
416}