use std::borrow::Cow;
use std::env::current_dir;
use std::ffi::OsStr;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use itertools::Itertools;
use thiserror::Error;
use tracing::warn;
use crate::code_source::CodeSource;
pub enum Loader {
File(Vec<(File, PathBuf)>),
Memory(Vec<String>),
}
#[derive(Error, Debug)]
pub enum LoadingError {
#[error("Provided path should be absolute")]
StartingPathNotAbsolute,
#[error("IO error: {0}")]
IOError(#[from] std::io::Error),
}
pub struct LoaderBuilder<'p> {
starting_path: Cow<'p, Path>,
extension: Cow<'p, OsStr>,
accept_any_extension: bool,
#[allow(dead_code)]
cache_extension: &'p OsStr,
}
impl Default for LoaderBuilder<'static> {
fn default() -> Self {
Self {
starting_path: Cow::Owned(
current_dir().expect("Cannot get absolute path starting from here"),
),
extension: Cow::Borrowed(OsStr::new("kd")),
accept_any_extension: false,
cache_extension: OsStr::new("kdc"),
}
}
}
impl<'p> LoaderBuilder<'p> {
#[must_use]
pub fn with_starting_path<P: Into<Cow<'p, Path>>>(self, path: P) -> Self {
Self {
starting_path: path.into(),
..self
}
}
#[must_use]
pub fn with_extension<S: Into<Cow<'p, OsStr>>>(self, extension: S) -> Self {
Self {
extension: extension.into(),
..self
}
}
#[must_use]
pub fn with_any_source_extension(self) -> Self {
Self {
accept_any_extension: true,
..self
}
}
pub fn build(self) -> Result<Loader, LoadingError> {
let sources = if self.starting_path.is_dir() {
if !self.starting_path.is_absolute() {
return Err(LoadingError::StartingPathNotAbsolute);
}
self.starting_path
.read_dir()
.map_err(LoadingError::IOError)?
.filter_ok(|it| {
if !it.path().is_file() {
false
} else if self.accept_any_extension {
true
} else {
it.path()
.extension()
.is_some_and(|ext| ext == self.extension)
}
})
.filter_map_ok(|it| {
let file = match File::open(it.path()) {
Ok(f) => f,
Err(e) => {
warn!("Skipping file {0} because: {1}", it.path().display(), e);
return None;
}
};
Some((file, it.path()))
})
.try_collect()?
} else if self.starting_path.is_file()
&& self
.starting_path
.extension()
.is_some_and(|ext| ext == self.extension)
{
vec![(
File::open(&self.starting_path)?,
self.starting_path.into_owned(),
)]
} else {
vec![]
};
Ok(Loader::File(sources))
}
}
impl Loader {
#[must_use]
pub fn file<'b>() -> LoaderBuilder<'b> {
LoaderBuilder::default()
}
pub fn from_single_snippet<S: Into<String>>(text: S) -> Self {
Self::Memory(vec![text.into()])
}
pub fn from_text(text: impl IntoIterator<Item = String>) -> Self {
Self::Memory(text.into_iter().collect())
}
fn generate_scratch_name(index: usize) -> String {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
format!("scratch-#{index}-{time}.kd", time = timestamp.as_millis())
}
#[must_use]
pub fn into_sources(self) -> Vec<CodeSource> {
match self {
Loader::File(sources) => sources
.into_iter()
.map(|it| CodeSource::file(it.1, it.0))
.collect(),
Loader::Memory(sources) => sources
.into_iter()
.enumerate()
.map(|(i, it)| CodeSource::memory(Self::generate_scratch_name(i), it))
.collect(),
}
}
}
impl Default for Loader {
fn default() -> Self {
Self::Memory(Default::default())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use std::env::temp_dir;
use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;
use tempfile::tempfile;
use crate::loader::Loader;
#[test]
fn test_load_text_from_scratch() {
let text = "Hello world";
let loader = Loader::from_single_snippet(text);
let mut sources = loader.into_sources();
assert_eq!(sources.len(), 1);
let mut source = sources.pop().unwrap();
let mut output = String::new();
source.read_to_string(&mut output).unwrap();
assert_eq!(output, text);
}
fn suite<P: AsRef<Path> + ?Sized>(file: &mut File, filepath: &P) {
let loader = Loader::file().with_starting_path(filepath.as_ref()).build();
assert!(loader.is_ok());
let loader = loader.unwrap();
let text = "Hello world";
write!(file, "{0}", text).unwrap();
let mut sources = loader.into_sources();
assert_eq!(sources.len(), 1);
let mut source = sources.pop().unwrap();
let mut output = String::new();
source.read_to_string(&mut output).unwrap();
assert_eq!(output, text);
}
#[test]
fn test_load_from_file_by_folder() {
let mut file = tempfile::Builder::new().suffix(".kd").tempfile().unwrap();
suite(file.as_file_mut(), &temp_dir());
file.close().unwrap();
}
#[test]
fn test_load_from_file_by_concrete_file() {
let mut file = tempfile::Builder::new().suffix(".kd").tempfile().unwrap();
let path = file.path().to_owned();
suite(file.as_file_mut(), &path);
file.close().unwrap();
}
#[test]
fn test_load_any_temp_file() {
let _ = tempfile();
let loader = Loader::file()
.with_starting_path(&temp_dir())
.with_any_source_extension()
.build()
.unwrap();
let sources = loader.into_sources();
assert!(!sources.is_empty())
}
}