use anyhow::{Context, Result, bail};
use quarb::{AllowShell, NodeId, QueryResult, WithNow};
#[cfg(feature = "native")]
use std::path::Path;
#[cfg(feature = "native")]
use std::rc::Rc;
#[derive(Clone, Copy, Default)]
pub struct Options {
pub hidden: bool,
pub respect_ignore: bool,
pub descend: bool,
}
pub enum Doc {
Json(quarb_json::JsonAdapter),
Csv(quarb_csv::CsvAdapter),
Xml(quarb_xml::XmlAdapter),
Html(quarb_html::HtmlAdapter),
#[cfg(feature = "native")]
Sqlite(quarb_sqlite::SqliteAdapter),
#[cfg(feature = "native")]
Fs(quarb_fs::FsAdapter),
#[cfg(feature = "native")]
FsDeep(quarb_compose::ComposeAdapter<quarb_fs::FsAdapter>),
#[cfg(feature = "native")]
Git(quarb_git::GitAdapter),
#[cfg(feature = "native")]
Archive(quarb_compose::ComposeAdapter<quarb_archive::ArchiveAdapter>),
#[cfg(feature = "native")]
Xlsx(quarb_xlsx::XlsxAdapter),
#[cfg(feature = "native")]
Code(quarb_code::CodeAdapter),
#[cfg(feature = "native")]
Mount(quarb_mount::MountAdapter),
}
impl Doc {
pub fn parse(input: &str, format: &str) -> Result<Doc> {
match format {
"json" => quarb_json::JsonAdapter::parse(input)
.map(Doc::Json)
.context("parsing JSON"),
"yaml" | "yml" => quarb_yaml::parse(input).map(Doc::Json).context("parsing YAML"),
"toml" => quarb_toml::parse(input).map(Doc::Json).context("parsing TOML"),
"csv" => quarb_csv::CsvAdapter::parse_with_delimiter(input, b',')
.map(Doc::Csv)
.context("parsing CSV"),
"tsv" => quarb_csv::CsvAdapter::parse_with_delimiter(input, b'\t')
.map(Doc::Csv)
.context("parsing TSV"),
"xml" => quarb_xml::XmlAdapter::parse(input)
.map(Doc::Xml)
.context("parsing XML"),
"html" => Ok(Doc::Html(quarb_html::HtmlAdapter::parse(input))),
"markdown" | "md" => Ok(Doc::Html(quarb_markdown::parse(input))),
other => bail!("unknown format: {other}"),
}
}
pub fn run(&self, query: &str, now: (i64, u32), allow_shell: bool) -> quarb::Result<QueryResult> {
let (secs, nanos) = now;
macro_rules! go {
($a:expr) => {{
let nowed = WithNow {
inner: $a,
secs,
nanos,
};
if allow_shell {
quarb::run(query, &AllowShell { inner: &nowed })
} else {
quarb::run(query, &nowed)
}
}};
}
match self {
Doc::Json(a) => go!(a),
Doc::Csv(a) => go!(a),
Doc::Xml(a) => go!(a),
Doc::Html(a) => go!(a),
#[cfg(feature = "native")]
Doc::Sqlite(a) => go!(a),
#[cfg(feature = "native")]
Doc::Fs(a) => go!(a),
#[cfg(feature = "native")]
Doc::FsDeep(a) => go!(a),
#[cfg(feature = "native")]
Doc::Git(a) => go!(a),
#[cfg(feature = "native")]
Doc::Archive(a) => go!(a),
#[cfg(feature = "native")]
Doc::Xlsx(a) => go!(a),
#[cfg(feature = "native")]
Doc::Code(a) => go!(a),
#[cfg(feature = "native")]
Doc::Mount(a) => go!(a),
}
}
pub fn render(&self, node: NodeId) -> String {
match self {
Doc::Json(a) => a.pointer(node),
Doc::Csv(a) => a.locator(node),
Doc::Xml(a) => a.locator(node),
Doc::Html(a) => a.locator(node),
#[cfg(feature = "native")]
Doc::Sqlite(a) => a.locator(node),
#[cfg(feature = "native")]
Doc::Fs(a) => a.path(node).display().to_string(),
#[cfg(feature = "native")]
Doc::FsDeep(a) => a.locator(node, |o| a.outer().path(o).display().to_string()),
#[cfg(feature = "native")]
Doc::Git(a) => a.locator(node),
#[cfg(feature = "native")]
Doc::Archive(a) => a.locator(node, |o| a.outer().locator(o)),
#[cfg(feature = "native")]
Doc::Xlsx(a) => a.locator(node),
#[cfg(feature = "native")]
Doc::Code(a) => a.locator(node),
#[cfg(feature = "native")]
Doc::Mount(a) => generic_locator(a, node),
}
}
}
#[cfg(feature = "native")]
impl Doc {
pub fn open(path: &Path, opts: &Options) -> Result<Doc> {
if path.is_dir() {
let fsopts = quarb_fs::FsOptions {
hidden: opts.hidden,
respect_ignore: opts.respect_ignore,
};
let fs = quarb_fs::FsAdapter::with_options(path, fsopts)
.with_context(|| format!("opening directory {}", path.display()))?;
return Ok(if opts.descend {
Doc::FsDeep(quarb_compose::ComposeAdapter::new(fs))
} else {
Doc::Fs(fs)
});
}
let s = path.to_string_lossy();
if let Some(repo) = s.strip_prefix("git:") {
let a =
quarb_git::GitAdapter::open(Path::new(repo)).context("opening git repository")?;
return Ok(Doc::Git(a));
}
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_ascii_lowercase());
if let Some(e) = &ext
&& quarb_code::supported(e)
{
let a = quarb_code::CodeAdapter::open(path).context("parsing source file")?;
return Ok(Doc::Code(a));
}
if matches!(ext.as_deref(), Some("xlsx" | "xls" | "ods")) {
let a = quarb_xlsx::XlsxAdapter::open(path).context("opening workbook")?;
return Ok(Doc::Xlsx(a));
}
if is_sqlite(path) {
let a = quarb_sqlite::SqliteAdapter::open(path).context("opening SQLite database")?;
return Ok(Doc::Sqlite(a));
}
if is_archive(path) {
let a = quarb_archive::ArchiveAdapter::open(path).context("opening archive")?;
return Ok(Doc::Archive(quarb_compose::ComposeAdapter::new(a)));
}
let text = std::fs::read_to_string(path)
.with_context(|| format!("reading {}", path.display()))?;
let text = text
.strip_prefix('\u{feff}')
.map(str::to_owned)
.unwrap_or(text);
match ext.as_deref() {
Some("csv") => Doc::parse(&text, "csv"),
Some("tsv") => Doc::parse(&text, "tsv"),
Some("yaml" | "yml") => Doc::parse(&text, "yaml"),
Some("toml") => Doc::parse(&text, "toml"),
Some("md" | "markdown") => Doc::parse(&text, "markdown"),
_ => {
if is_xml(path, &text) {
Doc::parse(&text, "xml")
} else if is_html(path, &text) {
Doc::parse(&text, "html")
} else {
Doc::parse(&text, "json")
}
}
}
}
pub fn mount(paths: &[std::path::PathBuf], opts: &Options) -> Result<Doc> {
let mut mounts: Vec<quarb_mount::Mount> = Vec::new();
for (i, p) in paths.iter().enumerate() {
let stem = p
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| format!("doc{i}"));
if mounts.iter().any(|m| m.name == stem) {
bail!(
"input '{}' mounts as '{stem}', colliding with an earlier input of the \
same file stem; give each a distinct basename",
p.display()
);
}
let adapter = Doc::open(p, opts)?.into_boxed()?;
mounts.push(quarb_mount::Mount { name: stem, adapter });
}
Ok(Doc::Mount(quarb_mount::MountAdapter::new(mounts)))
}
fn into_boxed(self) -> Result<Box<dyn quarb::AstAdapter>> {
use quarb_mount::Shared;
Ok(match self {
Doc::Json(a) => Box::new(Shared(Rc::new(a))),
Doc::Csv(a) => Box::new(Shared(Rc::new(a))),
Doc::Xml(a) => Box::new(Shared(Rc::new(a))),
Doc::Html(a) => Box::new(Shared(Rc::new(a))),
Doc::Sqlite(a) => Box::new(Shared(Rc::new(a))),
Doc::Fs(a) => Box::new(Shared(Rc::new(a))),
Doc::FsDeep(a) => Box::new(Shared(Rc::new(a))),
Doc::Git(a) => Box::new(Shared(Rc::new(a))),
Doc::Archive(a) => Box::new(Shared(Rc::new(a))),
Doc::Xlsx(a) => Box::new(Shared(Rc::new(a))),
Doc::Code(a) => Box::new(Shared(Rc::new(a))),
Doc::Mount(_) => bail!("cannot nest a mount inside a mount"),
})
}
}
#[cfg(feature = "native")]
fn generic_locator<A: quarb::AstAdapter>(a: &A, node: NodeId) -> String {
let mut parts = Vec::new();
let mut cur = Some(node);
while let Some(n) = cur {
if let Some(nm) = a.name(n) {
parts.push(nm);
}
cur = a.parent(n);
}
parts.reverse();
format!("/{}", parts.join("/"))
}
#[cfg(feature = "native")]
fn is_sqlite(path: &Path) -> bool {
if path
.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "db" | "sqlite" | "sqlite3"))
{
return true;
}
use std::io::Read as _;
let mut buf = [0u8; 16];
std::fs::File::open(path)
.and_then(|mut f| f.read_exact(&mut buf))
.is_ok()
&& &buf == b"SQLite format 3\0"
}
#[cfg(feature = "native")]
fn is_archive(path: &Path) -> bool {
if path.extension().and_then(|e| e.to_str()).is_some_and(|e| {
matches!(
e.to_ascii_lowercase().as_str(),
"zip" | "tar" | "gz" | "tgz" | "jar" | "war" | "docx" | "pptx" | "odt" | "odp"
)
}) {
return true;
}
use std::io::Read as _;
let mut buf = [0u8; 2];
std::fs::File::open(path)
.and_then(|mut f| f.read_exact(&mut buf))
.is_ok()
&& (&buf == b"PK" || buf == [0x1f, 0x8b])
}
#[cfg(feature = "native")]
fn is_xml(path: &Path, text: &str) -> bool {
path.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "xml" | "svg" | "xhtml"))
|| text.trim_start().starts_with("<?xml")
}
#[cfg(feature = "native")]
fn is_html(path: &Path, text: &str) -> bool {
path.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "html" | "htm"))
|| text.trim_start().starts_with('<')
}