use std::cmp::Ordering;
use std::convert::TryFrom;
use std::ffi::OsStr;
use std::fmt::Display;
use std::path::Path;
use minijinja::value::{Object, Value};
use minijinja::{Environment, Error, ErrorKind, State};
use os_str_bytes::OsStrBytes;
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
pub fn get_env() -> Environment<'static> {
let mut env = Environment::new();
env.add_function("load_text", load_text);
env.add_function("file_list", file_list);
env.add_filter("dirs_first", dirs_first);
env.add_filter("sort_files", sort_files);
env.add_filter("lines", lines);
env.add_filter("split", split);
env
}
#[derive(Debug, Clone)]
struct FileEntry {
name: String,
stem: String,
path: String,
is_dir: bool,
}
impl Display for FileEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)
}
}
impl Object for FileEntry {
fn get_attr(&self, name: &str) -> Option<Value> {
match name {
"name" => Some(self.name.to_owned().into()),
"stem" => Some(self.stem.to_owned().into()),
"path" => Some(self.path.to_owned().into()),
"is_dir" => Some(self.is_dir.into()),
_ => None,
}
}
fn attributes(&self) -> &[&str] {
&["name", "stem", "path", "is_dir"]
}
}
fn file_list(state: &State) -> Result<Vec<Value>, Error> {
let file_path = state.lookup("path").ok_or_else(|| {
Error::new(
ErrorKind::NonKey,
"Variable `path` not in template, this is a bug in stargazer",
)
})?;
let file_path = String::try_from(file_path).map_err(|_| {
Error::new(
ErrorKind::BadSerialization,
"Variable `path` is not a string, this is a bug in stargazer",
)
})?;
let dir_path = Path::new(&file_path).parent().ok_or_else(|| {
Error::new(
ErrorKind::UndefinedError,
format!(
"This script's path `{}` does not have a parent directory",
file_path
),
)
})?;
let dir_iter = dir_path.read_dir().map_err(|_| {
Error::new(
ErrorKind::UndefinedError,
format!("Error reading dir `{}`", dir_path.display()),
)
})?;
let mut file_list = vec![];
for entry in dir_iter {
let entry = entry.map_err(|_| {
Error::new(
ErrorKind::UndefinedError,
format!("Error reading entry for dir `{}`", dir_path.display()),
)
})?;
if entry.path().extension() == Some(OsStr::new("tmpl")) {
continue;
}
let mut file_name = entry.file_name().to_string_lossy().into_owned();
let mut file_stem = entry
.path()
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or("".to_owned());
let is_dir = entry
.file_type()
.map_err(|_| {
Error::new(
ErrorKind::UndefinedError,
format!(
"Can't get file type of `{}`",
entry.path().display()
),
)
})?
.is_dir();
if is_dir {
file_name.push('/');
file_stem.push('/');
}
let file_path = format!(
"./{}",
percent_encode(
entry.file_name().to_raw_bytes().as_ref(),
NON_ALPHANUMERIC
)
);
let file_entry = FileEntry {
name: file_name,
path: file_path,
stem: file_stem,
is_dir,
};
file_list.push(Value::from_object(file_entry));
}
Ok(file_list)
}
fn dirs_first(
_state: &State,
file_entries: Vec<Value>,
) -> Result<Vec<Value>, Error> {
let mut entries: Vec<FileEntry> = vec![];
for entry in file_entries {
entries.push(
entry
.downcast_object_ref::<FileEntry>()
.ok_or_else(|| {
Error::new(
ErrorKind::UndefinedError,
"Items not file entries",
)
})?
.clone(),
);
}
entries.sort_by(|a, b| match (a.is_dir, b.is_dir) {
(true, true) | (false, false) => Ordering::Equal,
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
});
Ok(entries
.into_iter()
.map(|entry| Value::from_object(entry))
.collect())
}
fn sort_files(
_state: &State,
file_entries: Vec<Value>,
) -> Result<Vec<Value>, Error> {
let mut entries: Vec<FileEntry> = vec![];
for entry in file_entries {
entries.push(
entry
.downcast_object_ref::<FileEntry>()
.ok_or_else(|| {
Error::new(
ErrorKind::UndefinedError,
"Items not file entries",
)
})?
.clone(),
);
}
entries.sort_by(|a, b| a.name.cmp(&b.name));
Ok(entries
.into_iter()
.map(|entry| Value::from_object(entry))
.collect())
}
fn load_text(state: &State, path: String) -> Result<String, Error> {
let file_path = state.lookup("path").ok_or_else(|| {
Error::new(
ErrorKind::NonKey,
"Variable `path` not in template, this is a bug in stargazer",
)
})?;
let file_path = String::try_from(file_path).map_err(|_| {
Error::new(
ErrorKind::BadSerialization,
"Variable `path` is not a string, this is a bug in stargazer",
)
})?;
let dir_path = Path::new(&file_path).parent().ok_or_else(|| {
Error::new(
ErrorKind::UndefinedError,
format!(
"This script's path `{}` does not have a parent directory",
file_path
),
)
})?;
let file_path = dir_path.join(path);
match std::fs::read_to_string(&file_path) {
Ok(content) => Ok(content),
Err(e) => Err(Error::new(
ErrorKind::UndefinedError,
format!("Error loading file content: {:#}", e),
)),
}
}
fn lines(_state: &State, text: String) -> Result<Vec<String>, Error> {
Ok(text.lines().map(|s| s.to_owned()).collect())
}
fn split(_state: &State, text: String, pattern: String) -> Result<Vec<String>, Error> {
Ok(text.split(&pattern).map(|s| s.to_owned()).collect())
}