Improve config loading process and tests
This commit is contained in:
parent
7262727823
commit
4143caff3d
|
@ -1,4 +1,6 @@
|
|||
use std::collections::HashSet;
|
||||
use std::{collections::HashSet, path::Path};
|
||||
use thiserror::Error;
|
||||
use anyhow::Result;
|
||||
|
||||
mod path;
|
||||
mod parse;
|
||||
|
@ -8,18 +10,36 @@ mod store;
|
|||
|
||||
pub trait Config {
|
||||
fn label(&self) -> &str;
|
||||
fn match_paths(&self) -> &HashSet<String>;
|
||||
fn match_paths(&self) -> &[String];
|
||||
|
||||
fn is_match(&self, app: &AppProperties) -> bool;
|
||||
}
|
||||
|
||||
pub trait ConfigStore<'a> {
|
||||
fn default(&'a self) -> &'a dyn Config;
|
||||
fn active(&'a self, app: &AppProperties) -> &'a dyn Config;
|
||||
pub trait ConfigStore {
|
||||
fn default(&self) -> &dyn Config;
|
||||
fn active<'a>(&'a self, app: &AppProperties) -> &'a dyn Config;
|
||||
|
||||
fn get_all_match_paths(&self) -> HashSet<String>;
|
||||
}
|
||||
|
||||
pub struct AppProperties<'a> {
|
||||
pub title: Option<&'a str>,
|
||||
pub class: Option<&'a str>,
|
||||
pub exec: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub fn load_store(config_dir: &Path) -> Result<impl ConfigStore> {
|
||||
store::DefaultConfigStore::load(config_dir)
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ConfigStoreError {
|
||||
#[error("invalid config directory")]
|
||||
InvalidConfigDir(),
|
||||
|
||||
#[error("missing default.yml config")]
|
||||
MissingDefault(),
|
||||
|
||||
#[error("io error")]
|
||||
IOError(#[from] std::io::Error),
|
||||
}
|
|
@ -15,7 +15,7 @@ pub(crate) struct ResolvedConfig {
|
|||
|
||||
// Generated properties
|
||||
|
||||
match_paths: HashSet<String>,
|
||||
match_paths: Vec<String>,
|
||||
|
||||
filter_title: Option<Regex>,
|
||||
filter_class: Option<Regex>,
|
||||
|
@ -26,7 +26,7 @@ impl Default for ResolvedConfig {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
parsed: Default::default(),
|
||||
match_paths: HashSet::new(),
|
||||
match_paths: Vec::new(),
|
||||
filter_title: None,
|
||||
filter_class: None,
|
||||
filter_exec: None,
|
||||
|
@ -39,7 +39,7 @@ impl Config for ResolvedConfig {
|
|||
self.parsed.label.as_deref().unwrap_or("none")
|
||||
}
|
||||
|
||||
fn match_paths(&self) -> &HashSet<String> {
|
||||
fn match_paths(&self) -> &[String] {
|
||||
&self.match_paths
|
||||
}
|
||||
|
||||
|
@ -110,7 +110,7 @@ impl ResolvedConfig {
|
|||
.parent()
|
||||
.ok_or_else(ResolveError::ParentResolveFailed)?;
|
||||
|
||||
let match_paths = Self::generate_match_paths(&config, base_dir);
|
||||
let match_paths = Self::generate_match_paths(&config, base_dir).into_iter().collect();
|
||||
|
||||
let filter_title = if let Some(filter_title) = config.filter_title.as_deref() {
|
||||
Some(Regex::new(filter_title)?)
|
||||
|
@ -425,12 +425,17 @@ mod tests {
|
|||
|
||||
let config = ResolvedConfig::load(&config_file, None).unwrap();
|
||||
|
||||
let mut expected = HashSet::new();
|
||||
expected.insert(base_file.to_string_lossy().to_string());
|
||||
expected.insert(another_file.to_string_lossy().to_string());
|
||||
expected.insert(sub_file.to_string_lossy().to_string());
|
||||
let mut expected = vec![
|
||||
base_file.to_string_lossy().to_string(),
|
||||
another_file.to_string_lossy().to_string(),
|
||||
sub_file.to_string_lossy().to_string(),
|
||||
];
|
||||
expected.sort();
|
||||
|
||||
let mut result = config.match_paths().to_vec();
|
||||
result.sort();
|
||||
|
||||
assert_eq!(config.match_paths(), &expected);
|
||||
assert_eq!(result, expected.as_slice());
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -476,16 +481,21 @@ mod tests {
|
|||
let parent = ResolvedConfig::load(&parent_file, None).unwrap();
|
||||
let child = ResolvedConfig::load(&config_file, Some(&parent)).unwrap();
|
||||
|
||||
let mut expected = HashSet::new();
|
||||
expected.insert(sub_file.to_string_lossy().to_string());
|
||||
expected.insert(sub_under_file.to_string_lossy().to_string());
|
||||
let mut expected = vec![
|
||||
sub_file.to_string_lossy().to_string(),
|
||||
sub_under_file.to_string_lossy().to_string(),
|
||||
];
|
||||
expected.sort();
|
||||
|
||||
assert_eq!(child.match_paths(), &expected);
|
||||
let mut result = child.match_paths().to_vec();
|
||||
result.sort();
|
||||
assert_eq!(result, expected.as_slice());
|
||||
|
||||
let mut expected = HashSet::new();
|
||||
expected.insert(base_file.to_string_lossy().to_string());
|
||||
let expected = vec![
|
||||
base_file.to_string_lossy().to_string()
|
||||
];
|
||||
|
||||
assert_eq!(parent.match_paths(), &expected);
|
||||
assert_eq!(parent.match_paths(), expected.as_slice());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
use super::{Config, ConfigStore};
|
||||
use super::{Config, ConfigStore, ConfigStoreError, resolve::ResolvedConfig};
|
||||
use anyhow::Result;
|
||||
use log::{debug, error};
|
||||
use std::{collections::HashSet, path::Path};
|
||||
|
||||
pub(crate) struct DefaultConfigStore {
|
||||
default: Box<dyn Config>,
|
||||
customs: Vec<Box<dyn Config>>,
|
||||
}
|
||||
|
||||
impl<'a> ConfigStore<'a> for DefaultConfigStore {
|
||||
fn default(&'a self) -> &'a dyn super::Config {
|
||||
impl ConfigStore for DefaultConfigStore {
|
||||
fn default(&self) -> &dyn super::Config {
|
||||
self.default.as_ref()
|
||||
}
|
||||
|
||||
fn active(&'a self, app: &super::AppProperties) -> &'a dyn super::Config {
|
||||
fn active<'a>(&'a self, app: &super::AppProperties) -> &'a dyn super::Config {
|
||||
// Find a custom config that matches or fallback to the default one
|
||||
for custom in self.customs.iter() {
|
||||
if custom.is_match(app) {
|
||||
|
@ -19,6 +22,64 @@ impl<'a> ConfigStore<'a> for DefaultConfigStore {
|
|||
}
|
||||
self.default.as_ref()
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
fn get_all_match_paths(&self) -> HashSet<String> {
|
||||
let mut paths = HashSet::new();
|
||||
|
||||
paths.extend(self.default().match_paths().iter().cloned());
|
||||
for custom in self.customs.iter() {
|
||||
paths.extend(custom.match_paths().iter().cloned());
|
||||
}
|
||||
|
||||
paths
|
||||
}
|
||||
}
|
||||
|
||||
impl DefaultConfigStore {
|
||||
// TODO: test
|
||||
pub fn load(config_dir: &Path) -> Result<Self> {
|
||||
if !config_dir.is_dir() {
|
||||
return Err(ConfigStoreError::InvalidConfigDir().into());
|
||||
}
|
||||
|
||||
// First get the default.yml file
|
||||
let default_file = config_dir.join("default.yml");
|
||||
if !default_file.exists() || !default_file.is_file() {
|
||||
return Err(ConfigStoreError::MissingDefault().into());
|
||||
}
|
||||
let default = ResolvedConfig::load(&default_file, None)?;
|
||||
debug!("loaded default config at path: {:?}", default_file);
|
||||
|
||||
// Then the others
|
||||
let mut customs: Vec<Box<dyn Config>> = Vec::new();
|
||||
for entry in std::fs::read_dir(config_dir).map_err(ConfigStoreError::IOError)? {
|
||||
let entry = entry?;
|
||||
let config_file = entry.path();
|
||||
let extension = config_file.extension().unwrap_or_default().to_string_lossy().to_lowercase();
|
||||
|
||||
// Additional config files are loaded best-effort
|
||||
if config_file.is_file()
|
||||
&& config_file != default_file
|
||||
&& (extension == "yml" || extension == "yaml")
|
||||
{
|
||||
match ResolvedConfig::load(&config_file, Some(&default)) {
|
||||
Ok(config) => {
|
||||
customs.push(Box::new(config));
|
||||
debug!("loaded config at path: {:?}", config_file);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("unable to load config at path: {:?}, with error: {}", config_file, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
default: Box::new(default),
|
||||
customs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -44,7 +105,7 @@ mod tests {
|
|||
&self.label
|
||||
}
|
||||
|
||||
fn match_paths(&self) -> &std::collections::HashSet<String> {
|
||||
fn match_paths(&self) -> &[String] {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
|
|
|
@ -16,10 +16,109 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::path::Path;
|
||||
use config::ConfigStore;
|
||||
use matches::store::MatchStore;
|
||||
use anyhow::Result;
|
||||
use thiserror::Error;
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
mod util;
|
||||
mod config;
|
||||
mod matches;
|
||||
mod counter;
|
||||
pub mod config;
|
||||
pub mod matches;
|
||||
|
||||
pub fn load(base_path: &Path) -> Result<(impl ConfigStore, impl MatchStore)> {
|
||||
let config_dir = base_path.join("config");
|
||||
if !config_dir.exists() || !config_dir.is_dir() {
|
||||
return Err(ConfigError::MissingConfigDir().into())
|
||||
}
|
||||
|
||||
let config_store = config::load_store(&config_dir)?;
|
||||
let root_paths = config_store.get_all_match_paths();
|
||||
|
||||
let mut match_store = matches::store::new();
|
||||
match_store.load(&root_paths.into_iter().collect::<Vec<String>>());
|
||||
|
||||
Ok((config_store, match_store))
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ConfigError {
|
||||
#[error("missing config directory")]
|
||||
MissingConfigDir(),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use config::{AppProperties, ConfigStore};
|
||||
use crate::util::tests::use_test_directory;
|
||||
|
||||
#[test]
|
||||
fn load_works_correctly() {
|
||||
use_test_directory(|base, match_dir, config_dir| {
|
||||
let base_file = match_dir.join("base.yml");
|
||||
std::fs::write(&base_file, r#"
|
||||
matches:
|
||||
- trigger: "hello"
|
||||
replace: "world"
|
||||
"#).unwrap();
|
||||
|
||||
let another_file = match_dir.join("another.yml");
|
||||
std::fs::write(&another_file, r#"
|
||||
imports:
|
||||
- "_sub.yml"
|
||||
|
||||
matches:
|
||||
- trigger: "hello2"
|
||||
replace: "world2"
|
||||
"#).unwrap();
|
||||
|
||||
let under_file = match_dir.join("_sub.yml");
|
||||
std::fs::write(&under_file, r#"
|
||||
matches:
|
||||
- trigger: "hello3"
|
||||
replace: "world3"
|
||||
"#).unwrap();
|
||||
|
||||
let config_file = config_dir.join("default.yml");
|
||||
std::fs::write(&config_file, "").unwrap();
|
||||
|
||||
let custom_config_file = config_dir.join("custom.yml");
|
||||
std::fs::write(&custom_config_file, r#"
|
||||
filter_title: "Chrome"
|
||||
|
||||
use_standard_includes: false
|
||||
includes: ["../match/another.yml"]
|
||||
"#).unwrap();
|
||||
|
||||
let (config_store, match_store) = load(&base).unwrap();
|
||||
|
||||
assert_eq!(config_store.default().match_paths().len(), 2);
|
||||
assert_eq!(config_store.active(&AppProperties {
|
||||
title: Some("Google Chrome"),
|
||||
class: None,
|
||||
exec: None,
|
||||
}).match_paths().len(), 1);
|
||||
|
||||
assert_eq!(match_store.query(config_store.default().match_paths()).matches.len(), 3);
|
||||
assert_eq!(match_store.query(config_store.active(&AppProperties {
|
||||
title: Some("Chrome"),
|
||||
class: None,
|
||||
exec: None,
|
||||
}).match_paths()).matches.len(), 2);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_without_valid_config_dir() {
|
||||
use_test_directory(|_, match_dir, _| {
|
||||
// To correcly load the configs, the "load" method looks for the "config" directory
|
||||
assert!(load(&match_dir).is_err());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use serde_yaml::Mapping;
|
|||
use crate::counter::{next_id, StructId};
|
||||
|
||||
mod group;
|
||||
mod store;
|
||||
pub mod store;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Match {
|
||||
|
|
Loading…
Reference in New Issue
Block a user