use std::{
collections::BTreeMap,
path::{Path, PathBuf},
};
use anyhow::Result;
use ghee_lang::Key;
use path_absolutize::Absolutize;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::XATTR_TABLE_INFO;
#[derive(Error, Debug)]
enum TableInfoErr {
#[error("An io error occurred")]
IoError(std::io::Error),
#[error("A json error occurred: {0}")]
JsonError(serde_json::Error),
#[error("Path {0:?} does not exist")]
NoSuchPath(PathBuf),
}
#[derive(Clone, Serialize, Deserialize)]
pub struct TableInfo {
key: Key,
indices_abs: BTreeMap<Key, PathBuf>,
}
impl TableInfo {
pub fn new<P: AsRef<Path>>(key: Key, path: P) -> Self {
let abs_path = path.as_ref().absolutize().unwrap().to_path_buf();
let mut indices_abs = BTreeMap::new();
indices_abs.insert(key.clone(), abs_path.clone());
Self { key, indices_abs }
}
pub fn key(&self) -> &Key {
&self.key
}
pub fn path_abs(&self) -> &PathBuf {
&self.indices_abs[&self.key]
}
pub fn add_index(&mut self, key: Key, path: PathBuf) {
let abs_path = path.absolutize().unwrap().to_path_buf();
debug_assert!(abs_path.is_absolute());
self.indices_abs.insert(key.clone(), abs_path.clone());
}
pub fn indices_abs(&self) -> &BTreeMap<Key, PathBuf> {
debug_assert!(self.indices_abs.values().all(|p| p.is_absolute()));
&self.indices_abs
}
pub fn index_path_abs(&self, key: &Key) -> &PathBuf {
&self.indices_abs[key]
}
}
pub fn table_info<P: AsRef<Path>>(dir: P) -> Result<Option<TableInfo>> {
let dir = dir.as_ref();
if !dir.exists() {
return Err(TableInfoErr::NoSuchPath(dir.to_path_buf()).into());
}
if !dir.is_dir() {
return Ok(None);
}
let raw = xattr::get(dir, XATTR_TABLE_INFO.to_osstring())
.map_err(TableInfoErr::IoError)?
.unwrap_or_default();
if raw.is_empty() {
return Ok(None);
}
let info: TableInfo =
serde_json::from_slice(raw.as_slice()).map_err(TableInfoErr::JsonError)?;
debug_assert!(info.indices_abs.contains_key(&info.key));
debug_assert_eq!(info.path_abs(), &dir.absolutize().unwrap().to_path_buf());
Ok(Some(info))
}
pub fn set_table_info<P: AsRef<Path>>(dir: P, info: &TableInfo) -> Result<()> {
let json = serde_json::to_string(&info).map_err(TableInfoErr::JsonError)?;
Ok(
xattr::set(dir, XATTR_TABLE_INFO.to_osstring(), json.as_bytes())
.map_err(TableInfoErr::IoError)?,
)
}
pub fn containing_table_info<P: AsRef<Path>>(path: P) -> Result<Option<TableInfo>> {
let path = path.as_ref();
if !path.exists() {
return Err(TableInfoErr::NoSuchPath(path.into()).into());
}
let mut path = path;
if path.is_file() {
path = path.parent().unwrap();
}
debug_assert!(path.is_dir());
if let Some(table_info) = table_info(&path)? {
return Ok(Some(table_info));
}
let mut abs_path = path.absolutize().unwrap().to_path_buf();
loop {
match abs_path.parent() {
None => return Ok(None),
Some(parent) => {
match table_info(parent)? {
Some(parent_info) => return Ok(Some(parent_info)),
None => { }
};
abs_path.pop();
}
}
}
}
#[cfg(test)]
mod test {
use std::{
env::current_dir,
fs::{create_dir_all, File},
};
use ghee_lang::{Key, Namespace, Xattr};
use super::{containing_table_info, set_table_info, table_info, TableInfo};
use crate::{
cmd::init,
test_support::{CurrentDirGuard, TempDirAuto},
};
#[test]
fn test_key_roundtrip() {
let dir = TempDirAuto::new("ghee-test-key-roundtrip");
assert!(table_info(&dir).unwrap().is_none());
let key_component = Xattr::new(Namespace::User, "pizza");
let key = Key::new(vec![key_component]);
let info = TableInfo::new(key.clone(), dir.to_path_buf());
set_table_info(&dir, &info).unwrap();
assert_eq!(table_info(&dir).unwrap().unwrap().key, key);
}
#[test]
fn test_table_info() {
let dir1 = TempDirAuto::new("ghee-test-table-info");
let key1 = Key::from_string("region,city");
init(&dir1, &key1, false).unwrap();
let dir2 = {
let mut dir = dir1.clone();
dir.push("pnw");
dir
};
let dir3 = {
let mut dir = dir2.clone();
dir.push("Portland");
dir
};
create_dir_all(&dir3).unwrap();
let dir4 = {
let mut dir = dir3.clone();
dir.push("blahblahblah");
dir
};
{
let info = table_info(&dir1).unwrap().unwrap();
assert!(info.indices_abs.contains_key(&key1));
}
for dir in vec![&dir2, &dir3] {
let info = table_info(dir).unwrap();
assert!(info.is_none());
}
assert!(
table_info(&dir4).is_err(),
"dir4 doesn't exist and so should error when getting table info"
);
{
let file_path = {
let mut p = dir3.clone();
p.push("README.md");
p
};
File::create(&file_path).unwrap();
assert!(
table_info(&file_path).unwrap().is_none(),
"Files per se have no table info and so should error"
);
}
}
#[test]
fn test_containing_table_info() {
let cur = current_dir().unwrap();
let dir1 = TempDirAuto::new("ghee-test-containing-table-info");
let prior = CurrentDirGuard::new(&dir1);
let key1 = Key::from_string("region,city");
init(&dir1, &key1, false).unwrap();
let dir2 = {
let mut dir = dir1.clone();
dir.push("pnw");
dir
};
let dir3 = {
let mut dir = dir2.clone();
dir.push("Portland");
dir
};
create_dir_all(&dir3).unwrap();
let file = {
let mut p = dir3.clone();
p.push("README.md");
File::create(&p).unwrap();
p
};
let dir4 = {
let mut dir = dir3.clone();
dir.push("blahblahblah");
dir
};
for dir in vec![&dir1, &dir2, &dir3, &file] {
let info = containing_table_info(dir).unwrap().unwrap();
assert_eq!(info.key, key1);
assert!(info.indices_abs.contains_key(&key1));
}
assert!(
containing_table_info(&dir4).is_err(),
"dir4 doesn't exist and so should error when getting table info"
);
assert_eq!(prior.prior_dir, cur);
}
}