use crate::dsl::{DivoomDslOperationResource, DivoomDslOperationResourceLoader};
use crate::{DivoomAPIError, DivoomAPIResult};
use log::{debug, warn};
use rand::Rng;
use std::cmp::min;
use std::fs;
use std::mem::swap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
pub(crate) struct DivoomDslOperationNoOpResourceLoader {}
impl DivoomDslOperationNoOpResourceLoader {
pub fn new() -> Box<dyn DivoomDslOperationResourceLoader + Send> {
Box::new(DivoomDslOperationNoOpResourceLoader {})
}
}
impl DivoomDslOperationResourceLoader for DivoomDslOperationNoOpResourceLoader {
fn next(&mut self) -> DivoomAPIResult<Arc<DivoomDslOperationResource>> {
Err(DivoomAPIError::ResourceLoadError {
source: std::io::Error::new(
std::io::ErrorKind::Unsupported,
"This operation doesn't support loading any resources.",
),
})
}
}
pub(crate) struct DivoomDslOperationFileResourceLoader {
file_path: String,
file_content: Mutex<Option<Arc<DivoomDslOperationResource>>>,
}
impl DivoomDslOperationFileResourceLoader {
pub fn new(file_path: &str) -> Box<dyn DivoomDslOperationResourceLoader + Send> {
Box::new(DivoomDslOperationFileResourceLoader {
file_path: file_path.to_string(),
file_content: Mutex::new(None),
})
}
}
impl DivoomDslOperationResourceLoader for DivoomDslOperationFileResourceLoader {
fn next(&mut self) -> DivoomAPIResult<Arc<DivoomDslOperationResource>> {
let mut file_content = self.file_content.lock().unwrap();
if file_content.is_none() {
*file_content = Some(Arc::new(DivoomDslOperationResource::new(
&self.file_path,
fs::read(&self.file_path)?,
)));
}
Ok(file_content.as_ref().unwrap().clone())
}
}
pub(crate) struct DivoomDslOperationGlobResourceLoader {
file_pattern: String,
random: bool,
prefetch_count: usize,
file_path_candidates: Mutex<Vec<PathBuf>>,
file_resources: Mutex<Vec<Arc<DivoomDslOperationResource>>>,
}
impl DivoomDslOperationGlobResourceLoader {
pub fn new(
file_pattern: &str,
random: bool,
prefetch_count: usize,
) -> Box<dyn DivoomDslOperationResourceLoader + Send> {
Box::new(DivoomDslOperationGlobResourceLoader {
file_pattern: file_pattern.to_string(),
random,
prefetch_count,
file_path_candidates: Mutex::new(Vec::new()),
file_resources: Mutex::new(Vec::new()),
})
}
fn load_next_file_content_batch(
file_pattern: &str,
random: bool,
fetch_count: usize,
file_path_candidates: &mut Vec<PathBuf>,
) -> DivoomAPIResult<Vec<Arc<DivoomDslOperationResource>>> {
debug!(
"Loading new batch of file paths: Pattern = {}, Random = {}, FetchCount = {}",
file_pattern, random, fetch_count
);
if file_path_candidates.is_empty() {
debug!("File path candidates are running out, rescanning disk.");
let glob_matches = match glob::glob(file_pattern) {
Err(e) => return Err(DivoomAPIError::ParameterError(e.to_string())),
Ok(v) => v,
};
for file_entry in glob_matches {
if let Ok(file_path) = file_entry {
file_path_candidates.push(file_path);
}
}
debug!(
"{} files found with file pattern: {}",
file_path_candidates.len(),
file_pattern
);
}
if file_path_candidates.is_empty() {
return Ok(Vec::new());
}
let mut files_to_fetch: Vec<PathBuf>;
if !random {
let file_to_fetch_count = min(file_path_candidates.len(), fetch_count as usize);
files_to_fetch = file_path_candidates.split_off(file_to_fetch_count);
swap(&mut files_to_fetch, file_path_candidates);
} else {
files_to_fetch = Vec::new();
let mut rng = rand::thread_rng();
while !file_path_candidates.is_empty() && files_to_fetch.len() < fetch_count {
let index = rng.gen_range(0..file_path_candidates.len());
let file_path = file_path_candidates.swap_remove(index);
files_to_fetch.push(file_path);
}
}
debug!("Selected {} files to load.", files_to_fetch.len());
let mut file_resources = Vec::new();
for file_path in files_to_fetch {
let file_content = match fs::read(&file_path) {
Err(e) => {
warn!("Failed to load file, skip failed and continue loading more: Path = {:?}, Error = {:?}", file_path, e);
continue;
}
Ok(v) => v,
};
let file_resource = Arc::new(DivoomDslOperationResource::new(
file_path.to_str().unwrap(),
file_content,
));
file_resources.push(file_resource);
}
file_resources.reverse();
Ok(file_resources)
}
}
impl DivoomDslOperationResourceLoader for DivoomDslOperationGlobResourceLoader {
fn next(&mut self) -> DivoomAPIResult<Arc<DivoomDslOperationResource>> {
let mut guarded_file_resources = self.file_resources.lock().unwrap();
if guarded_file_resources.is_empty() {
let mut guarded_file_path_candidates = self.file_path_candidates.lock().unwrap();
*guarded_file_resources =
DivoomDslOperationGlobResourceLoader::load_next_file_content_batch(
&self.file_pattern,
self.random,
self.prefetch_count,
&mut guarded_file_path_candidates,
)?;
}
if guarded_file_resources.is_empty() {
return Err(DivoomAPIError::ResourceLoadError {
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"Unable to load any resources, no files are found.",
),
});
}
let last_file_index = guarded_file_resources.len() - 1;
let resource = guarded_file_resources.remove(last_file_index);
Ok(resource)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dsl_resource_loader_can_load_single_file() {
let mut loader = DivoomDslOperationFileResourceLoader::new(
"test_data/dsl_runner_tests/system_commands.json",
);
run_dsl_resource_loader_test(
&mut loader,
&vec!["test_data/dsl_runner_tests/system_commands.json".to_string()],
);
}
#[test]
fn dsl_resource_loader_can_load_file_with_pattern() {
let mut loader = DivoomDslOperationGlobResourceLoader::new(
"test_data/dsl_runner_tests/*.json",
false,
5,
);
run_dsl_resource_loader_test(
&mut loader,
&vec![
"test_data/dsl_runner_tests/animation_commands.json".to_string(),
"test_data/dsl_runner_tests/batch_commands.json".to_string(),
"test_data/dsl_runner_tests/channel_commands.json".to_string(),
"test_data/dsl_runner_tests/raw_commands.json".to_string(),
"test_data/dsl_runner_tests/system_commands.json".to_string(),
"test_data/dsl_runner_tests/tool_commands.json".to_string(),
"test_data/dsl_runner_tests/animation_commands.json".to_string(),
],
);
}
fn run_dsl_resource_loader_test(
loader: &mut Box<dyn DivoomDslOperationResourceLoader + Send>,
expected_resource_name_suffixes: &Vec<String>,
) {
for expected_resource_name_suffix in expected_resource_name_suffixes {
let resource = loader.next().unwrap();
let normalized_resource_name = resource.name.replace('\\', "/");
assert!(normalized_resource_name.ends_with(expected_resource_name_suffix));
assert!(!resource.data.is_empty());
}
}
}