use std::path::{Path, PathBuf};
use crate::FileFormat;
pub use crate::formats::sqlite::{Pick, Table};
pub fn holder(path: &Path) -> Option<FileFormat> {
crate::formats::readers::sniff_file(path, crate::formats::readers::Asked::Tables)
}
pub fn place(file: &Path, name: &str) -> PathBuf {
let mut place = file.as_os_str().to_owned();
place.push("/");
place.push(name);
place.into()
}
pub fn split(path: &Path) -> Option<(PathBuf, String)> {
split_inside(path, |file| holder(file).is_some())
}
pub fn split_variant(path: &Path, formats: &crate::formats::Registry) -> Option<(PathBuf, String)> {
if formats.is_empty() {
return None;
}
let (file, name) = split_inside(path, |file| formats.variants_of(file).is_some())?;
let (_, tables) = variants(&file, formats)?;
let found = tables.iter().find(|t| t.name == name).or_else(|| {
let mut alike = tables.iter().filter(|t| t.name.eq_ignore_ascii_case(&name));
match (alike.next(), alike.next()) {
(Some(one), None) => Some(one),
_ => None,
}
})?;
Some((file, found.name.clone()))
}
pub fn variants(file: &Path, formats: &crate::formats::Registry) -> Option<(String, Vec<Table>)> {
let spec = formats.variants_of(file)?;
Some((spec.name.clone(), variant_tables(&spec)))
}
pub fn variant_tables(spec: &crate::formats::Spec) -> Vec<Table> {
let named = |fields: &[crate::formats::Field]| {
fields
.iter()
.filter_map(|f| f.name.clone())
.collect::<Vec<_>>()
};
let common = named(&spec.records.fields);
spec.records
.variants
.iter()
.map(|v| {
let columns = common.iter().cloned().chain(named(&v.fields));
Table::plain(&v.name, "record type", columns)
})
.collect()
}
fn split_inside(path: &Path, holds: impl Fn(&Path) -> bool) -> Option<(PathBuf, String)> {
if path.file_name().is_some() && path.exists() {
return None;
}
let file = path
.ancestors()
.skip(1)
.take_while(|p| !p.as_os_str().is_empty())
.find(|p| p.file_name().is_some() && p.is_file())?;
if !holds(file) {
return None;
}
let rest = path.to_str()?.strip_prefix(file.to_str()?)?;
let mut chars = rest.chars();
chars.next().filter(|c| std::path::is_separator(*c))?;
let name = chars.as_str().trim_end_matches(std::path::is_separator);
(!name.is_empty()).then(|| (file.to_path_buf(), name.to_string()))
}
pub fn tables(file: &Path, format: FileFormat) -> color_eyre::Result<Vec<Table>> {
match crate::formats::readers::of(format).tables {
Some(tables) => tables(file),
None => Err(color_eyre::eyre::eyre!(
"A {} file holds one table.",
format.name()
)),
}
.map_err(|e| crate::error_display::in_file(file, e))
}
pub fn pick(
tables: Vec<Table>,
wanted: Option<&str>,
display: &Path,
empty: &str,
) -> color_eyre::Result<Pick> {
use crate::error_display::FileError;
let own: Vec<&Table> = tables.iter().filter(|t| !t.internal).collect();
let names = || {
const SHOWN: usize = 20;
let mut names: Vec<&str> = own.iter().take(SHOWN).map(|t| t.name.as_str()).collect();
let more = own.len().saturating_sub(SHOWN);
let more = format!("and {more} more");
if own.len() > SHOWN {
names.push(&more);
}
names.join(", ")
};
if let Some(wanted) = wanted {
let found = tables.iter().find(|t| t.name == wanted).or_else(|| {
let mut alike = tables
.iter()
.filter(|t| t.name.eq_ignore_ascii_case(wanted));
match (alike.next(), alike.next()) {
(Some(one), None) => Some(one),
_ => None,
}
});
return match found {
Some(table) => Ok(Pick::One(table.clone())),
None if own.is_empty() => Err(FileError::new(
display,
format!("no table \"{wanted}\"; the file holds no tables."),
)
.into()),
None => Err(FileError::new(
display,
format!(
"no table \"{wanted}\". --table names one of its tables: {}.",
names()
),
)
.into()),
};
}
match own.as_slice() {
[] => Err(FileError::new(display, format!("the file holds no tables.{empty}")).into()),
[one] => Ok(Pick::One((*one).clone())),
_ => Ok(Pick::Several(tables)),
}
}
#[derive(Default)]
pub struct Opened {
pub window: Option<(
std::sync::Arc<dyn crate::formats::pushdown::Windowed>,
usize,
)>,
pub detail: Option<std::sync::Arc<crate::formats::text_formats::Detail>>,
pub other_tables: Vec<String>,
pub notes: Vec<crate::notes::Note>,
pub units: Vec<(String, String)>,
pub indexing: Option<std::sync::Arc<crate::formats::lines::Lines>>,
pub numbering: Option<std::sync::Arc<crate::formats::lines::Lines>>,
}
impl Opened {
pub fn for_table(
detail: crate::formats::text_formats::Detail,
tables: &[Table],
picked: &str,
notes: Vec<String>,
scope: &str,
) -> Self {
Self {
detail: Some(std::sync::Arc::new(detail)),
other_tables: others(tables, picked),
notes: notes
.into_iter()
.map(|n| crate::formats::text_formats::note(n, scope.to_string()))
.collect(),
..Default::default()
}
}
pub(crate) fn scan(
self,
input: crate::formats::readers::ScanIn<'_>,
lf: polars::prelude::LazyFrame,
) -> crate::loading::scan::Scan {
input.report.opened = Some(std::sync::Arc::new(self));
lf.into()
}
}
pub(crate) fn several(
input: &crate::formats::readers::ScanIn<'_>,
tables: Vec<Table>,
) -> crate::loading::scan::Scan {
crate::loading::scan::Scan::Tables {
file: input.path().to_path_buf(),
tables: tables
.into_iter()
.filter(|t| !t.internal)
.map(|t| t.name)
.collect(),
format: input.format,
}
}
impl std::fmt::Debug for Opened {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Opened")
.field("rows", &self.window.as_ref().map(|(_, rows)| rows))
.field("other_tables", &self.other_tables)
.finish_non_exhaustive()
}
}
pub fn others(tables: &[Table], opened: &str) -> Vec<String> {
tables
.iter()
.filter(|t| !t.internal && t.name != opened)
.map(|t| t.name.clone())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_place_inside_a_file_reads_back_whole() {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("app.db");
let mut head = crate::formats::sqlite::MAGIC.to_vec();
head.resize(100, 0);
std::fs::write(&db, head).unwrap();
for name in ["users", "a/b", "/etc", "a/../b"] {
let place = place(&db, name);
assert_eq!(
split(&place),
Some((db.clone(), name.to_string())),
"{name}"
);
}
assert_eq!(split(&db), None);
let text = dir.path().join("notes.txt");
std::fs::write(&text, "hello").unwrap();
assert_eq!(split(&text.join("users")), None);
}
#[test]
fn a_place_inside_a_file_of_variants_names_one() {
let spec = crate::formats::Spec::parse(
r#"name = "acme.v"
match = { glob = ["*.v"] }
[records]
framing = "length_prefixed"
size = "len"
type = "kind"
fields = [{ name = "len", type = "u1" }, { name = "kind", type = "u1" }]
[[variants]]
name = "Add"
when = 1
fields = [{ name = "a", type = "u1" }]
[[variants]]
name = "exec"
when = 2
fields = [{ name = "b", type = "u1" }]"#,
None,
)
.unwrap();
let formats = crate::formats::Registry::of(vec![spec]);
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("day.v");
std::fs::write(&file, [3u8, 1, 7]).unwrap();
assert_eq!(
split_variant(&place(&file, "add"), &formats),
Some((file.clone(), "Add".to_string())),
"as the spec writes it"
);
assert_eq!(split_variant(&place(&file, "nope"), &formats), None);
assert_eq!(split_variant(&file, &formats), None);
let other = dir.path().join("day.w");
std::fs::write(&other, [0u8]).unwrap();
assert_eq!(split_variant(&place(&other, "exec"), &formats), None);
let (spec, tables) = variants(&file, &formats).unwrap();
assert_eq!(spec, "acme.v");
assert_eq!(tables[1].columns.len(), 3);
}
#[test]
fn pick_names_what_is_there() {
let t = |name: &str| Table::plain(name, "array", Vec::<String>::new());
let display = Path::new("run.npz");
let several = vec![t("x"), t("y")];
assert!(matches!(
pick(several.clone(), None, display, "").unwrap(),
Pick::Several(_)
));
assert_eq!(
pick(several.clone(), Some("Y"), display, "").unwrap(),
Pick::One(t("y"))
);
let missing = pick(several, Some("z"), display, "")
.unwrap_err()
.to_string();
assert_eq!(
missing,
"\"run.npz\": No table \"z\". --table names one of its tables: x, y."
);
let empty = pick(Vec::new(), None, display, "").unwrap_err().to_string();
assert_eq!(empty, "\"run.npz\": The file holds no tables.");
}
}