use std::{
cell::RefCell,
collections::HashMap,
fmt::Debug,
fs::{canonicalize, read_to_string},
io::{self, ErrorKind},
path::{Path, PathBuf, absolute},
};
use elsa::FrozenMap;
use crate::{DeclFile, DeclProvider, ParseOptions, errors::ImportError, parse_declaration_file};
pub struct FSProvider {
root: PathBuf,
parse_options: ParseOptions,
files: FrozenMap<u64, Box<DeclFile>>,
files_by_name: RefCell<HashMap<PathBuf, u64>>,
}
impl Debug for FSProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FSProvider")
.field("root", &self.root)
.field("parse_options", &self.parse_options)
.finish()
}
}
impl FSProvider {
pub fn new(root: impl Into<PathBuf>) -> io::Result<Self> {
FSProvider::with_options(root, ParseOptions { relative_paths: true, metadata: false })
}
pub fn with_options(root: impl Into<PathBuf>, parse_options: ParseOptions) -> io::Result<Self> {
Ok(Self {
root: canonicalize(root.into())?,
parse_options,
files: FrozenMap::new(),
files_by_name: RefCell::new(HashMap::new()),
})
}
pub fn load_file(&self, path: impl AsRef<Path>) -> Result<&DeclFile, ImportError> {
let path = absolute(Path::join(&self.root, path.as_ref()))
.map_err(|e| ImportError::Other(e.to_string()))?;
if let Some(id) = self.files_by_name.borrow().get(&path) {
return Ok(&self.files[id]);
}
if !path.starts_with(&self.root) {
let msg = format!("importing outside root \"{}\"", path.display());
return Err(ImportError::Other(msg));
}
let source = read_to_string(&path).map_err(|e| {
if e.kind() == ErrorKind::NotFound {
ImportError::NotFound
} else {
ImportError::Other(e.to_string())
}
})?;
let file_name = path.to_string_lossy().to_string();
let file = parse_declaration_file(&source, file_name, &self.parse_options, self)
.map_err(ImportError::Parse)?;
let id = file.id;
self.files_by_name.borrow_mut().insert(path, file.id);
self.files.insert(file.id, Box::new(file));
Ok(&self.files[&id])
}
}
impl DeclProvider for FSProvider {
fn get(&self, id: u64) -> &DeclFile {
&self.files[&id]
}
fn load<'a>(&'a self, name: &str) -> Result<&'a DeclFile, ImportError> {
self.load_file(name)
}
}