use crate::{emojis, models::Manifest};
use async_trait::async_trait;
use console::style;
use scan_dir::ScanDir;
use std::{
collections::HashMap,
error::Error,
fmt::{Display, Formatter},
path::{Path, PathBuf},
};
#[derive(Debug, Clone)]
pub enum ManifestError {
#[allow(dead_code)]
Generic,
Parse(String),
IO(String),
}
impl Error for ManifestError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
}
impl Display for ManifestError {
fn fmt(&self, f: &mut Formatter) -> Result<(), core::fmt::Error> {
match self {
Self::Generic => write!(f, "Generic Error"),
Self::Parse(s) => write!(f, "Error trying to parse the manifest: {}", s),
Self::IO(s) => write!(f, "Error trying to read a file/directory: {}", s),
}
}
}
impl From<toml::de::Error> for ManifestError {
fn from(error: toml::de::Error) -> Self {
Self::Parse(error.to_string())
}
}
impl From<std::io::Error> for ManifestError {
fn from(error: std::io::Error) -> Self {
Self::IO(error.to_string())
}
}
#[async_trait]
pub trait ManifestReader {
async fn read_manifest(&self) -> Result<Manifest, ManifestError>;
async fn read_assets(&self, assets_dir_path: &Path) -> Result<Vec<String>, ManifestError>;
}
pub struct TokioManifestReader<'a> {
manifest_path: &'a Path,
}
#[async_trait]
impl ManifestReader for TokioManifestReader<'_> {
async fn read_manifest(&self) -> Result<Manifest, ManifestError> {
let manifest_str = tokio::fs::read_to_string(&self.manifest_path).await?;
let manifest = toml::from_str(&manifest_str)?;
Ok(manifest)
}
async fn read_assets(&self, assets_dir_path: &Path) -> Result<Vec<String>, ManifestError> {
let mut all_files: Vec<String> = vec![];
let walk_result = ScanDir::files().walk(assets_dir_path, |wlkr| {
for (entry, _) in wlkr {
let entry_str = entry
.path()
.strip_prefix(assets_dir_path)
.expect("unable to remove prefix")
.display()
.to_string();
all_files.push(entry_str);
}
});
if let Err(errors) = walk_result {
return Err(ManifestError::IO(format!(
"Errors reading the assets directory: {:?}",
errors
)));
}
Ok(all_files)
}
}
pub struct ManifestChecker<T: ManifestReader> {
reader: T,
}
#[allow(clippy::use_self)]
impl<T: ManifestReader> ManifestChecker<T> {
pub fn new(reader: T) -> Self {
Self { reader }
}
pub fn with_tokio_reader(manifest_path: &Path) -> ManifestChecker<TokioManifestReader> {
ManifestChecker::new(TokioManifestReader { manifest_path })
}
pub async fn check(&self) -> Result<ManifestInfo, ManifestError> {
let manifest = self.reader.read_manifest().await?;
let assets_dir_path = std::env::current_dir()?.join(&manifest.path);
let assets = self.reader.read_assets(&assets_dir_path).await?;
Ok(Self::compare_results(manifest, assets, assets_dir_path))
}
fn compare_results(
manifest: Manifest,
assets: Vec<String>,
assets_dir_path: PathBuf,
) -> ManifestInfo {
let mut new_assets = vec![];
let extensions = &manifest.file_extensions;
let scales = &manifest.file_scales;
let mut manifest_map = manifest
.files
.into_iter()
.fold(HashMap::new(), |mut acc, file| {
let mut temp_files = vec![];
if Path::new(&file).extension().is_none() {
for ext in extensions {
temp_files.push(format!("{}.{}", file, ext));
}
} else {
temp_files.push(file);
}
let mut temp_files_scales = vec![];
for scale in scales.iter().filter(|&s| s > &1) {
for tf in &temp_files {
temp_files_scales.push(format!("{}.0x/{}", scale, tf));
}
}
temp_files.append(&mut temp_files_scales);
for temp_file in temp_files {
acc.insert(temp_file, false);
}
acc
});
for asset in assets {
if let Some(x) = manifest_map.get_mut(&asset) {
*x = true;
} else {
new_assets.push(asset.clone());
}
}
let mut missing_assets = manifest_map
.into_iter()
.filter_map(|(key, val)| if val { None } else { Some(key) })
.collect::<Vec<_>>();
missing_assets.sort();
new_assets.sort();
ManifestInfo::default()
.with_assets_dir_path(assets_dir_path)
.with_new_assets(new_assets)
.with_missing_assets(missing_assets)
}
}
#[derive(Default, Debug)]
pub struct ManifestInfo {
pub assets_dir_path: Option<PathBuf>,
pub new_assets: Option<Vec<String>>,
pub missing_assets: Option<Vec<String>>,
}
impl ManifestInfo {
pub fn with_assets_dir_path(mut self, assets_dir_path: PathBuf) -> Self {
self.assets_dir_path = Some(assets_dir_path);
self
}
pub fn with_new_assets(mut self, assets: Vec<String>) -> Self {
if !assets.is_empty() {
self.new_assets = Some(assets);
}
self
}
pub fn with_missing_assets(mut self, assets: Vec<String>) -> Self {
if !assets.is_empty() {
self.missing_assets = Some(assets);
}
self
}
pub fn print_info(&self) {
if let Some(missing_assets) = &self.missing_assets {
println!(
"{} {}",
emojis::ERROR,
style("There are some assets missing").red().bold()
);
for asset in missing_assets {
println!(" {}", style(asset).red());
}
}
if let Some(new_assets) = &self.new_assets {
println!(
"{} {}",
emojis::PLANT,
style("There are some new assets").green().bold()
);
for asset in new_assets {
println!(" {}", style(asset).green());
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockReader {
pub read_manifest_result: Manifest,
pub read_assets_result: Vec<String>,
}
impl MockReader {
fn new(manifest_result: Manifest, assets_result: Vec<String>) -> Self {
Self {
read_manifest_result: manifest_result,
read_assets_result: assets_result,
}
}
}
#[async_trait]
impl ManifestReader for MockReader {
async fn read_manifest(&self) -> Result<Manifest, ManifestError> {
Ok(self.read_manifest_result.clone())
}
async fn read_assets(&self, _: &Path) -> Result<Vec<String>, ManifestError> {
Ok(self.read_assets_result.clone())
}
}
fn asset_builder(vec: Vec<&str>) -> Vec<String> {
vec.into_iter().map(String::from).collect()
}
#[tokio::test]
async fn manifest_should_have_all_none_if_assets_and_manifest_are_ok() -> anyhow::Result<()> {
let assets = asset_builder(vec!["a.jpg", "b.jpg"]);
let manifest = Manifest::default().with_files(assets.clone());
let mock_reader = MockReader::new(manifest, assets);
let checker = ManifestChecker::new(mock_reader);
let result = checker.check().await?;
assert!(result.new_assets.is_none());
assert!(result.missing_assets.is_none());
Ok(())
}
#[tokio::test]
async fn manifest_should_have_new_assets_if_new_assets_are_added() -> anyhow::Result<()> {
let manifest = Manifest::default().with_files(asset_builder(vec!["a.jpg", "b.jpg"]));
let assets = asset_builder(vec!["a.jpg", "b.jpg", "c.jpg"]);
let mock_reader = MockReader::new(manifest, assets.clone());
let checker = ManifestChecker::new(mock_reader);
let result = checker.check().await?;
assert!(result.new_assets.is_some());
assert_eq!(result.new_assets.unwrap()[0], assets[2]);
assert!(result.missing_assets.is_none());
Ok(())
}
#[tokio::test]
async fn manifest_should_have_missing_assets_if_assets_are_missing() -> anyhow::Result<()> {
let manifest_files = asset_builder(vec!["a.jpg", "b.jpg"]);
let manifest = Manifest::default().with_files(manifest_files.clone());
let assets = asset_builder(vec!["a.jpg"]);
let mock_reader = MockReader::new(manifest, assets);
let checker = ManifestChecker::new(mock_reader);
let result = checker.check().await?;
assert!(result.new_assets.is_none());
assert!(result.missing_assets.is_some());
assert_eq!(result.missing_assets.unwrap()[0], manifest_files[1]);
Ok(())
}
#[tokio::test]
async fn manifest_should_use_extensions_when_files_have_no_extension() -> anyhow::Result<()> {
let manifest = Manifest {
file_extensions: vec!["png".to_string(), "svg".to_string()],
file_scales: vec![1],
files: asset_builder(vec!["a", "b"]),
path: "".to_owned(),
};
let assets = asset_builder(vec!["a.png", "a.svg", "b.png", "b.svg"]);
let mock_reader = MockReader::new(manifest, assets);
let checker = ManifestChecker::new(mock_reader);
let result = checker.check().await?;
assert!(result.new_assets.is_none());
assert!(result.missing_assets.is_none());
Ok(())
}
#[tokio::test]
async fn manifest_should_not_use_extensions_when_files_have_extensions() -> anyhow::Result<()> {
let manifest = Manifest {
file_extensions: vec!["png".to_string(), "svg".to_string()],
file_scales: vec![1],
files: asset_builder(vec!["a", "b", "c.svg", "d.jpg"]),
path: "".to_owned(),
};
let assets = asset_builder(vec!["a.png", "a.svg", "b.png", "b.svg", "c.svg", "d.jpg"]);
let mock_reader = MockReader::new(manifest, assets);
let checker = ManifestChecker::new(mock_reader);
let result = checker.check().await?;
assert!(result.new_assets.is_none());
assert!(result.missing_assets.is_none());
Ok(())
}
#[tokio::test]
async fn manifest_should_use_scales() -> anyhow::Result<()> {
let manifest = Manifest {
file_extensions: vec!["png".to_string(), "svg".to_string()],
file_scales: vec![1, 2, 3],
files: asset_builder(vec!["a"]),
path: "".to_owned(),
};
let assets = asset_builder(vec![
"a.png",
"a.svg",
"2.0x/a.png",
"2.0x/a.svg",
"3.0x/a.png",
"3.0x/a.svg",
]);
let mock_reader = MockReader::new(manifest, assets);
let checker = ManifestChecker::new(mock_reader);
let result = checker.check().await?;
assert!(result.new_assets.is_none());
assert!(result.missing_assets.is_none());
Ok(())
}
}