1use anyhow::{Context, Result, bail};
17use quarb::{AllowShell, NodeId, QueryResult, WithNow};
18
19#[cfg(feature = "native")]
20use std::path::Path;
21use std::rc::Rc;
22
23#[derive(Clone, Copy, Default)]
26pub struct Options {
27 pub hidden: bool,
28 pub respect_ignore: bool,
29 pub descend: bool,
30}
31
32pub enum Doc {
36 Json(quarb_json::JsonAdapter),
37 Csv(quarb_csv::CsvAdapter),
38 Xml(quarb_xml::XmlAdapter),
39 Html(quarb_html::HtmlAdapter),
40 Sqlite(quarb_sqlite::SqliteAdapter),
41 #[cfg(feature = "native")]
42 Fs(quarb_fs::FsAdapter),
43 #[cfg(feature = "native")]
44 FsDeep(quarb_compose::ComposeAdapter<quarb_fs::FsAdapter>),
45 #[cfg(feature = "native")]
46 Git(quarb_git::GitAdapter),
47 #[cfg(feature = "native")]
48 Archive(quarb_compose::ComposeAdapter<quarb_archive::ArchiveAdapter>),
49 #[cfg(feature = "native")]
50 Xlsx(quarb_xlsx::XlsxAdapter),
51 #[cfg(feature = "native")]
52 Code(quarb_code::CodeAdapter),
53 Mount(quarb_mount::MountAdapter),
54 #[cfg(feature = "native")]
58 Boxed(Dyn, Box<dyn Fn(NodeId) -> String>),
59}
60
61#[cfg(feature = "native")]
64pub struct Dyn(pub Box<dyn quarb::AstAdapter>);
65
66#[cfg(feature = "native")]
67impl quarb::AstAdapter for Dyn {
68 fn root(&self) -> NodeId {
69 self.0.root()
70 }
71 fn children(&self, node: NodeId) -> Vec<NodeId> {
72 self.0.children(node)
73 }
74 fn name(&self, node: NodeId) -> Option<String> {
75 self.0.name(node)
76 }
77 fn parent(&self, node: NodeId) -> Option<NodeId> {
78 self.0.parent(node)
79 }
80 fn traits(&self, node: NodeId) -> Vec<String> {
81 self.0.traits(node)
82 }
83 fn property(&self, node: NodeId, name: &str) -> Option<quarb::Value> {
84 self.0.property(node, name)
85 }
86 fn children_named(&self, node: NodeId, name: &str) -> Vec<NodeId> {
87 self.0.children_named(node, name)
88 }
89 fn default_value(&self, node: NodeId) -> Option<quarb::Value> {
90 self.0.default_value(node)
91 }
92 fn metadata(&self, node: NodeId, key: &str) -> Option<quarb::Value> {
93 self.0.metadata(node, key)
94 }
95 fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
96 self.0.links(node)
97 }
98 fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
99 self.0.backlinks(node)
100 }
101 fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
102 self.0.resolve(node, property, hint)
103 }
104 fn link_property(
105 &self,
106 source: NodeId,
107 label: &str,
108 target: NodeId,
109 name: &str,
110 ) -> Option<quarb::Value> {
111 self.0.link_property(source, label, target, name)
112 }
113 fn quantifier_bound(&self) -> usize {
114 self.0.quantifier_bound()
115 }
116 fn invocation_instant(&self) -> Option<(i64, u32)> {
117 self.0.invocation_instant()
118 }
119 fn unit_scale(&self, expr: &str) -> Option<(f64, String)> {
120 self.0.unit_scale(expr)
121 }
122}
123
124impl Doc {
125 pub fn parse(input: &str, format: &str) -> Result<Doc> {
129 match format {
130 "json" => quarb_json::JsonAdapter::parse(input)
131 .map(Doc::Json)
132 .context("parsing JSON"),
133 "jsonl" | "ndjson" => quarb_json::JsonAdapter::parse_lines(input)
134 .map(Doc::Json)
135 .context("parsing JSONL"),
136 "yaml" | "yml" => quarb_yaml::parse(input).map(Doc::Json).context("parsing YAML"),
137 "toml" => quarb_toml::parse(input).map(Doc::Json).context("parsing TOML"),
138 "csv" => quarb_csv::CsvAdapter::parse_with_delimiter(input, b',')
139 .map(Doc::Csv)
140 .context("parsing CSV"),
141 "tsv" => quarb_csv::CsvAdapter::parse_with_delimiter(input, b'\t')
142 .map(Doc::Csv)
143 .context("parsing TSV"),
144 "xml" => quarb_xml::XmlAdapter::parse(input)
145 .map(Doc::Xml)
146 .context("parsing XML"),
147 "html" => Ok(Doc::Html(quarb_html::HtmlAdapter::parse(input))),
148 "markdown" | "md" => Ok(Doc::Html(quarb_markdown::parse(input))),
149 other => bail!("unknown format: {other}"),
150 }
151 }
152
153 pub fn run(&self, query: &str, now: (i64, u32), allow_shell: bool) -> quarb::Result<QueryResult> {
158 let (secs, nanos) = now;
159 macro_rules! go {
160 ($a:expr) => {{
161 let nowed = WithNow {
162 inner: $a,
163 secs,
164 nanos,
165 };
166 if allow_shell {
167 quarb::run(query, &AllowShell { inner: &nowed })
168 } else {
169 quarb::run(query, &nowed)
170 }
171 }};
172 }
173 match self {
174 Doc::Json(a) => go!(a),
175 Doc::Csv(a) => go!(a),
176 Doc::Xml(a) => go!(a),
177 Doc::Html(a) => go!(a),
178 Doc::Sqlite(a) => go!(a),
179 #[cfg(feature = "native")]
180 Doc::Fs(a) => go!(a),
181 #[cfg(feature = "native")]
182 Doc::FsDeep(a) => go!(a),
183 #[cfg(feature = "native")]
184 Doc::Git(a) => go!(a),
185 #[cfg(feature = "native")]
186 Doc::Archive(a) => go!(a),
187 #[cfg(feature = "native")]
188 Doc::Xlsx(a) => go!(a),
189 #[cfg(feature = "native")]
190 Doc::Code(a) => go!(a),
191 Doc::Mount(a) => go!(a),
192 #[cfg(feature = "native")]
193 Doc::Boxed(a, _) => go!(a),
194 }
195 }
196
197 pub fn render(&self, node: NodeId) -> String {
199 match self {
200 Doc::Json(a) => a.pointer(node),
201 Doc::Csv(a) => a.locator(node),
202 Doc::Xml(a) => a.locator(node),
203 Doc::Html(a) => a.locator(node),
204 Doc::Sqlite(a) => a.locator(node),
205 #[cfg(feature = "native")]
206 Doc::Fs(a) => a.path(node).display().to_string(),
207 #[cfg(feature = "native")]
208 Doc::FsDeep(a) => a.locator(node, |o| a.outer().path(o).display().to_string()),
209 #[cfg(feature = "native")]
210 Doc::Git(a) => a.locator(node),
211 #[cfg(feature = "native")]
212 Doc::Archive(a) => a.locator(node, |o| a.outer().locator(o)),
213 #[cfg(feature = "native")]
214 Doc::Xlsx(a) => a.locator(node),
215 #[cfg(feature = "native")]
216 Doc::Code(a) => a.locator(node),
217 Doc::Mount(a) => generic_locator(a, node),
218 #[cfg(feature = "native")]
219 Doc::Boxed(_, render) => render(node),
220 }
221 }
222
223 pub fn sqlite_bytes(bytes: &[u8]) -> Result<Doc> {
226 Ok(Doc::Sqlite(
227 quarb_sqlite::SqliteAdapter::from_bytes(bytes)
228 .map_err(|e| anyhow::anyhow!("{e}"))
229 .context("opening SQLite bytes")?,
230 ))
231 }
232
233 pub fn mount_docs(parts: Vec<(String, Doc)>) -> Result<Doc> {
237 let mut mounts: Vec<quarb_mount::Mount> = Vec::new();
238 for (name, doc) in parts {
239 if mounts.iter().any(|m| m.name == name) {
240 bail!("two sources mount as '{name}'; give each a distinct name");
241 }
242 mounts.push(quarb_mount::Mount {
243 name,
244 adapter: doc.into_boxed()?,
245 });
246 }
247 Ok(Doc::Mount(quarb_mount::MountAdapter::new(mounts)))
248 }
249
250 pub fn mount_texts(parts: &[(String, String, String)]) -> Result<Doc> {
255 let mut docs: Vec<(String, Doc)> = Vec::new();
256 for (name, format, text) in parts {
257 let doc = Doc::parse(text, format)
258 .with_context(|| format!("parsing '{name}' as {format}"))?;
259 docs.push((name.clone(), doc));
260 }
261 Doc::mount_docs(docs)
262 }
263
264 fn into_boxed(self) -> Result<Box<dyn quarb::AstAdapter>> {
266 use quarb_mount::Shared;
267 Ok(match self {
268 Doc::Json(a) => Box::new(Shared(Rc::new(a))),
269 Doc::Csv(a) => Box::new(Shared(Rc::new(a))),
270 Doc::Xml(a) => Box::new(Shared(Rc::new(a))),
271 Doc::Html(a) => Box::new(Shared(Rc::new(a))),
272 Doc::Sqlite(a) => Box::new(Shared(Rc::new(a))),
273 #[cfg(feature = "native")]
274 Doc::Fs(a) => Box::new(Shared(Rc::new(a))),
275 #[cfg(feature = "native")]
276 Doc::FsDeep(a) => Box::new(Shared(Rc::new(a))),
277 #[cfg(feature = "native")]
278 Doc::Git(a) => Box::new(Shared(Rc::new(a))),
279 #[cfg(feature = "native")]
280 Doc::Archive(a) => Box::new(Shared(Rc::new(a))),
281 #[cfg(feature = "native")]
282 Doc::Xlsx(a) => Box::new(Shared(Rc::new(a))),
283 #[cfg(feature = "native")]
284 Doc::Code(a) => Box::new(Shared(Rc::new(a))),
285 Doc::Mount(_) => bail!("cannot nest a mount inside a mount"),
286 #[cfg(feature = "native")]
287 Doc::Boxed(a, _) => a.0,
288 })
289 }
290}
291
292#[cfg(feature = "native")]
297impl Doc {
298 pub fn open(path: &Path, opts: &Options) -> Result<Doc> {
304 if path.is_dir() {
305 let fsopts = quarb_fs::FsOptions {
306 hidden: opts.hidden,
307 respect_ignore: opts.respect_ignore,
308 };
309 let fs = quarb_fs::FsAdapter::with_options(path, fsopts)
310 .with_context(|| format!("opening directory {}", path.display()))?;
311 return Ok(if opts.descend {
312 Doc::FsDeep(quarb_compose::ComposeAdapter::with_source_paths(
313 fs,
314 |fs, n| Some(fs.path(n)),
315 ))
316 } else {
317 Doc::Fs(fs)
318 });
319 }
320
321 let s = path.to_string_lossy();
322 if let Some(repo) = s.strip_prefix("git:") {
323 let a =
324 quarb_git::GitAdapter::open(Path::new(repo)).context("opening git repository")?;
325 return Ok(Doc::Git(a));
326 }
327
328 let ext = path
329 .extension()
330 .and_then(|e| e.to_str())
331 .map(|e| e.to_ascii_lowercase());
332
333 if let Some(e) = &ext
334 && quarb_code::supported(e)
335 {
336 let a = quarb_code::CodeAdapter::open(path).context("parsing source file")?;
337 return Ok(Doc::Code(a));
338 }
339 if matches!(ext.as_deref(), Some("xlsx" | "xls" | "ods")) {
340 let a = quarb_xlsx::XlsxAdapter::open(path).context("opening workbook")?;
341 return Ok(Doc::Xlsx(a));
342 }
343 if is_sqlite(path) {
344 let a = quarb_sqlite::SqliteAdapter::open(path).context("opening SQLite database")?;
345 return Ok(Doc::Sqlite(a));
346 }
347 if is_archive(path) {
348 let a = quarb_archive::ArchiveAdapter::open(path).context("opening archive")?;
349 return Ok(Doc::Archive(quarb_compose::ComposeAdapter::new(a)));
350 }
351
352 let text = std::fs::read_to_string(path)
354 .with_context(|| format!("reading {}", path.display()))?;
355 let text = text
356 .strip_prefix('\u{feff}')
357 .map(str::to_owned)
358 .unwrap_or(text);
359 match ext.as_deref() {
360 Some("csv") => Doc::parse(&text, "csv"),
361 Some("tsv") => Doc::parse(&text, "tsv"),
362 Some("yaml" | "yml") => Doc::parse(&text, "yaml"),
363 Some("toml") => Doc::parse(&text, "toml"),
364 Some("md" | "markdown") => Doc::parse(&text, "markdown"),
365 Some("jsonl" | "ndjson") => Doc::parse(&text, "jsonl"),
366 _ => {
367 if is_xml(path, &text) {
368 Doc::parse(&text, "xml")
369 } else if is_html(path, &text) {
370 Doc::parse(&text, "html")
371 } else {
372 Doc::parse(&text, "json")
373 }
374 }
375 }
376 }
377
378 pub fn mount(paths: &[std::path::PathBuf], opts: &Options) -> Result<Doc> {
382 let specs: Vec<crate::MountSpec> = paths
383 .iter()
384 .map(|p| crate::MountSpec {
385 name: None,
386 path: p.clone(),
387 })
388 .collect();
389 Doc::mount_specs(&specs, opts)
390 }
391
392 pub fn mount_specs(specs: &[crate::MountSpec], opts: &Options) -> Result<Doc> {
395 let mut mounts: Vec<quarb_mount::Mount> = Vec::new();
396 for (i, spec) in specs.iter().enumerate() {
397 let name = spec.name.clone().unwrap_or_else(|| {
398 spec.path
399 .file_stem()
400 .map(|s| s.to_string_lossy().into_owned())
401 .unwrap_or_else(|| format!("doc{i}"))
402 });
403 if mounts.iter().any(|m| m.name == name) {
404 bail!(
405 "input '{}' mounts as '{name}', colliding with an earlier input of the \
406 same name; give each a distinct basename (or a NAME=TARGET alias)",
407 spec.path.display()
408 );
409 }
410 let adapter = Doc::open(&spec.path, opts)?.into_boxed()?;
411 mounts.push(quarb_mount::Mount { name, adapter });
412 }
413 Ok(Doc::Mount(quarb_mount::MountAdapter::new(mounts)))
414 }
415
416}
417
418fn generic_locator<A: quarb::AstAdapter>(a: &A, node: NodeId) -> String {
422 let mut parts = Vec::new();
423 let mut cur = Some(node);
424 while let Some(n) = cur {
425 if let Some(nm) = a.name(n) {
426 parts.push(nm);
427 }
428 cur = a.parent(n);
429 }
430 parts.reverse();
431 format!("/{}", parts.join("/"))
432}
433
434#[cfg(feature = "native")]
437fn is_sqlite(path: &Path) -> bool {
438 if path
439 .extension()
440 .and_then(|e| e.to_str())
441 .is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "db" | "sqlite" | "sqlite3"))
442 {
443 return true;
444 }
445 use std::io::Read as _;
446 let mut buf = [0u8; 16];
447 std::fs::File::open(path)
448 .and_then(|mut f| f.read_exact(&mut buf))
449 .is_ok()
450 && &buf == b"SQLite format 3\0"
451}
452
453#[cfg(feature = "native")]
455fn is_archive(path: &Path) -> bool {
456 if path.extension().and_then(|e| e.to_str()).is_some_and(|e| {
457 matches!(
458 e.to_ascii_lowercase().as_str(),
459 "zip" | "tar" | "gz" | "tgz" | "jar" | "war" | "docx" | "pptx" | "odt" | "odp"
460 )
461 }) {
462 return true;
463 }
464 use std::io::Read as _;
465 let mut buf = [0u8; 2];
466 std::fs::File::open(path)
467 .and_then(|mut f| f.read_exact(&mut buf))
468 .is_ok()
469 && (&buf == b"PK" || buf == [0x1f, 0x8b])
470}
471
472#[cfg(feature = "native")]
475fn is_xml(path: &Path, text: &str) -> bool {
476 path.extension()
477 .and_then(|e| e.to_str())
478 .is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "xml" | "svg" | "xhtml"))
479 || text.trim_start().starts_with("<?xml")
480}
481
482#[cfg(feature = "native")]
485fn is_html(path: &Path, text: &str) -> bool {
486 path.extension()
487 .and_then(|e| e.to_str())
488 .is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "html" | "htm"))
489 || text.trim_start().starts_with('<')
490}