Add config store base implementation and tests
This commit is contained in:
parent
0ca740914f
commit
7262727823
|
@ -4,8 +4,22 @@ mod path;
|
|||
mod parse;
|
||||
mod util;
|
||||
mod resolve;
|
||||
mod store;
|
||||
|
||||
pub trait Config {
|
||||
fn label(&self) -> &str;
|
||||
fn match_paths(&self) -> &HashSet<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 struct AppProperties<'a> {
|
||||
pub title: Option<&'a str>,
|
||||
pub class: Option<&'a str>,
|
||||
pub exec: Option<&'a str>,
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
use super::{parse::ParsedConfig, path::calculate_paths, Config};
|
||||
use super::{parse::ParsedConfig, path::calculate_paths, util::os_matches, AppProperties, Config};
|
||||
use crate::merge;
|
||||
use anyhow::Result;
|
||||
use regex::Regex;
|
||||
use std::iter::FromIterator;
|
||||
use std::{collections::HashSet, path::Path};
|
||||
use thiserror::Error;
|
||||
|
@ -8,12 +9,17 @@ use thiserror::Error;
|
|||
const STANDARD_INCLUDES: &[&str] = &["../match/**/*.yml"];
|
||||
const STANDARD_EXCLUDES: &[&str] = &["../match/**/_*.yml"];
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ResolvedConfig {
|
||||
parsed: ParsedConfig,
|
||||
|
||||
// Generated properties
|
||||
|
||||
match_paths: HashSet<String>,
|
||||
|
||||
filter_title: Option<Regex>,
|
||||
filter_class: Option<Regex>,
|
||||
filter_exec: Option<Regex>,
|
||||
}
|
||||
|
||||
impl Default for ResolvedConfig {
|
||||
|
@ -21,6 +27,9 @@ impl Default for ResolvedConfig {
|
|||
Self {
|
||||
parsed: Default::default(),
|
||||
match_paths: HashSet::new(),
|
||||
filter_title: None,
|
||||
filter_class: None,
|
||||
filter_exec: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +42,58 @@ impl Config for ResolvedConfig {
|
|||
fn match_paths(&self) -> &HashSet<String> {
|
||||
&self.match_paths
|
||||
}
|
||||
|
||||
fn is_match(&self, app: &AppProperties) -> bool {
|
||||
if self.parsed.filter_os.is_none()
|
||||
&& self.parsed.filter_title.is_none()
|
||||
&& self.parsed.filter_exec.is_none()
|
||||
&& self.parsed.filter_class.is_none()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let is_os_match = if let Some(filter_os) = self.parsed.filter_os.as_deref() {
|
||||
os_matches(filter_os)
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
let is_title_match =
|
||||
if let Some(title_regex) = self.filter_title.as_ref() {
|
||||
if let Some(title) = app.title {
|
||||
title_regex.is_match(title)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
let is_exec_match =
|
||||
if let Some(exec_regex) = self.filter_exec.as_ref() {
|
||||
if let Some(exec) = app.exec {
|
||||
exec_regex.is_match(exec)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
let is_class_match =
|
||||
if let Some(class_regex) = self.filter_class.as_ref() {
|
||||
if let Some(class) = app.class {
|
||||
class_regex.is_match(class)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
// All the filters that have been specified must be true to define a match
|
||||
is_os_match && is_exec_match && is_title_match && is_class_match
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolvedConfig {
|
||||
|
@ -47,13 +108,34 @@ impl ResolvedConfig {
|
|||
// Extract the base directory
|
||||
let base_dir = path
|
||||
.parent()
|
||||
.ok_or_else(|| ResolveError::ParentResolveFailed())?;
|
||||
.ok_or_else(ResolveError::ParentResolveFailed)?;
|
||||
|
||||
let match_paths = Self::generate_match_paths(&config, base_dir);
|
||||
|
||||
let filter_title = if let Some(filter_title) = config.filter_title.as_deref() {
|
||||
Some(Regex::new(filter_title)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let filter_class = if let Some(filter_class) = config.filter_class.as_deref() {
|
||||
Some(Regex::new(filter_class)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let filter_exec = if let Some(filter_exec) = config.filter_exec.as_deref() {
|
||||
Some(Regex::new(filter_exec)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
parsed: config,
|
||||
match_paths,
|
||||
filter_title,
|
||||
filter_class,
|
||||
filter_exec,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -210,9 +292,13 @@ mod tests {
|
|||
..Default::default()
|
||||
}),
|
||||
HashSet::from_iter(
|
||||
vec!["../match/**/*.yml".to_string(), "custom/*.yml".to_string(), "sub/*.yml".to_string()]
|
||||
.iter()
|
||||
.cloned()
|
||||
vec![
|
||||
"../match/**/*.yml".to_string(),
|
||||
"custom/*.yml".to_string(),
|
||||
"sub/*.yml".to_string()
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -277,9 +363,13 @@ mod tests {
|
|||
..Default::default()
|
||||
}),
|
||||
HashSet::from_iter(
|
||||
vec!["../match/**/_*.yml".to_string(), "custom/*.yml".to_string(), "sub/*.yml".to_string()]
|
||||
.iter()
|
||||
.cloned()
|
||||
vec![
|
||||
"../match/**/_*.yml".to_string(),
|
||||
"custom/*.yml".to_string(),
|
||||
"sub/*.yml".to_string()
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -364,16 +454,24 @@ mod tests {
|
|||
// Configs
|
||||
|
||||
let parent_file = config_dir.join("parent.yml");
|
||||
std::fs::write(&parent_file, r#"
|
||||
std::fs::write(
|
||||
&parent_file,
|
||||
r#"
|
||||
excludes: ['../**/another.yml']
|
||||
"#).unwrap();
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let config_file = config_dir.join("default.yml");
|
||||
std::fs::write(&config_file, r#"
|
||||
std::fs::write(
|
||||
&config_file,
|
||||
r#"
|
||||
use_standard_includes: false
|
||||
excludes: []
|
||||
includes: ["../match/sub/*.yml"]
|
||||
"#).unwrap();
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let parent = ResolvedConfig::load(&parent_file, None).unwrap();
|
||||
let child = ResolvedConfig::load(&config_file, Some(&parent)).unwrap();
|
||||
|
@ -390,4 +488,178 @@ mod tests {
|
|||
assert_eq!(parent.match_paths(), &expected);
|
||||
});
|
||||
}
|
||||
|
||||
fn test_filter_is_match(config: &str, app: &AppProperties) -> bool {
|
||||
let mut result = false;
|
||||
let result_ref = &mut result;
|
||||
use_test_directory(move |_, _, config_dir| {
|
||||
let config_file = config_dir.join("default.yml");
|
||||
std::fs::write(&config_file, config).unwrap();
|
||||
|
||||
let config = ResolvedConfig::load(&config_file, None).unwrap();
|
||||
|
||||
*result_ref = config.is_match(app)
|
||||
});
|
||||
result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_match_no_filters() {
|
||||
assert!(!test_filter_is_match(
|
||||
"",
|
||||
&AppProperties {
|
||||
title: Some("Google"),
|
||||
class: Some("Chrome"),
|
||||
exec: Some("chrome.exe"),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_match_filter_title() {
|
||||
assert!(test_filter_is_match(
|
||||
"filter_title: Google",
|
||||
&AppProperties {
|
||||
title: Some("Google Mail"),
|
||||
class: Some("Chrome"),
|
||||
exec: Some("chrome.exe"),
|
||||
},
|
||||
));
|
||||
|
||||
assert!(!test_filter_is_match(
|
||||
"filter_title: Google",
|
||||
&AppProperties {
|
||||
title: Some("Yahoo"),
|
||||
class: Some("Chrome"),
|
||||
exec: Some("chrome.exe"),
|
||||
},
|
||||
));
|
||||
|
||||
assert!(!test_filter_is_match(
|
||||
"filter_title: Google",
|
||||
&AppProperties {
|
||||
title: None,
|
||||
class: Some("Chrome"),
|
||||
exec: Some("chrome.exe"),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_match_filter_class() {
|
||||
assert!(test_filter_is_match(
|
||||
"filter_class: Chrome",
|
||||
&AppProperties {
|
||||
title: Some("Google Mail"),
|
||||
class: Some("Chrome"),
|
||||
exec: Some("chrome.exe"),
|
||||
},
|
||||
));
|
||||
|
||||
assert!(!test_filter_is_match(
|
||||
"filter_class: Chrome",
|
||||
&AppProperties {
|
||||
title: Some("Yahoo"),
|
||||
class: Some("Another"),
|
||||
exec: Some("chrome.exe"),
|
||||
},
|
||||
));
|
||||
|
||||
assert!(!test_filter_is_match(
|
||||
"filter_class: Chrome",
|
||||
&AppProperties {
|
||||
title: Some("google"),
|
||||
class: None,
|
||||
exec: Some("chrome.exe"),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_match_filter_exec() {
|
||||
assert!(test_filter_is_match(
|
||||
"filter_exec: chrome.exe",
|
||||
&AppProperties {
|
||||
title: Some("Google Mail"),
|
||||
class: Some("Chrome"),
|
||||
exec: Some("chrome.exe"),
|
||||
},
|
||||
));
|
||||
|
||||
assert!(!test_filter_is_match(
|
||||
"filter_exec: chrome.exe",
|
||||
&AppProperties {
|
||||
title: Some("Yahoo"),
|
||||
class: Some("Another"),
|
||||
exec: Some("zoom.exe"),
|
||||
},
|
||||
));
|
||||
|
||||
assert!(!test_filter_is_match(
|
||||
"filter_exec: chrome.exe",
|
||||
&AppProperties {
|
||||
title: Some("google"),
|
||||
class: Some("Chrome"),
|
||||
exec: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_match_filter_os() {
|
||||
let (current, another) = if cfg!(target_os = "windows") {
|
||||
("windows", "macos")
|
||||
} else if cfg!(target_os = "macos") {
|
||||
("macos", "windows")
|
||||
} else if cfg!(target_os = "linux") {
|
||||
("linux", "macos")
|
||||
} else {
|
||||
("invalid", "invalid")
|
||||
};
|
||||
|
||||
assert!(test_filter_is_match(
|
||||
&format!("filter_os: {}", current),
|
||||
&AppProperties {
|
||||
title: Some("Google Mail"),
|
||||
class: Some("Chrome"),
|
||||
exec: Some("chrome.exe"),
|
||||
},
|
||||
));
|
||||
|
||||
assert!(!test_filter_is_match(
|
||||
&format!("filter_os: {}", another),
|
||||
&AppProperties {
|
||||
title: Some("Google Mail"),
|
||||
class: Some("Chrome"),
|
||||
exec: Some("chrome.exe"),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_match_multiple_filters() {
|
||||
assert!(test_filter_is_match(
|
||||
r#"
|
||||
filter_exec: chrome.exe
|
||||
filter_title: "Youtube"
|
||||
"#,
|
||||
&AppProperties {
|
||||
title: Some("Youtube - Broadcast Yourself"),
|
||||
class: Some("Chrome"),
|
||||
exec: Some("chrome.exe"),
|
||||
},
|
||||
));
|
||||
|
||||
assert!(!test_filter_is_match(
|
||||
r#"
|
||||
filter_exec: chrome.exe
|
||||
filter_title: "Youtube"
|
||||
"#,
|
||||
&AppProperties {
|
||||
title: Some("Gmail"),
|
||||
class: Some("Chrome"),
|
||||
exec: Some("chrome.exe"),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
103
espanso-config/src/config/store.rs
Normal file
103
espanso-config/src/config/store.rs
Normal file
|
@ -0,0 +1,103 @@
|
|||
use super::{Config, ConfigStore};
|
||||
|
||||
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 {
|
||||
self.default.as_ref()
|
||||
}
|
||||
|
||||
fn active(&'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) {
|
||||
return custom.as_ref();
|
||||
}
|
||||
}
|
||||
self.default.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
struct MockConfig {
|
||||
label: String,
|
||||
is_match: bool,
|
||||
}
|
||||
|
||||
impl MockConfig {
|
||||
pub fn new(label: &str, is_match: bool) -> Self {
|
||||
Self {
|
||||
label: label.to_string(),
|
||||
is_match,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config for MockConfig {
|
||||
fn label(&self) -> &str {
|
||||
&self.label
|
||||
}
|
||||
|
||||
fn match_paths(&self) -> &std::collections::HashSet<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_match(&self, _: &crate::config::AppProperties) -> bool {
|
||||
self.is_match
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_store_selects_correctly() {
|
||||
let default = MockConfig::new("default", false);
|
||||
let custom1 = MockConfig::new("custom1", false);
|
||||
let custom2 = MockConfig::new("custom2", true);
|
||||
|
||||
let store = DefaultConfigStore {
|
||||
default: Box::new(default),
|
||||
customs: vec![Box::new(custom1), Box::new(custom2)],
|
||||
};
|
||||
|
||||
assert_eq!(store.default().label(), "default");
|
||||
assert_eq!(
|
||||
store
|
||||
.active(&crate::config::AppProperties {
|
||||
title: None,
|
||||
class: None,
|
||||
exec: None,
|
||||
})
|
||||
.label(),
|
||||
"custom2"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_store_active_fallback_to_default_if_no_match() {
|
||||
let default = MockConfig::new("default", false);
|
||||
let custom1 = MockConfig::new("custom1", false);
|
||||
let custom2 = MockConfig::new("custom2", false);
|
||||
|
||||
let store = DefaultConfigStore {
|
||||
default: Box::new(default),
|
||||
customs: vec![Box::new(custom1), Box::new(custom2)],
|
||||
};
|
||||
|
||||
assert_eq!(store.default().label(), "default");
|
||||
assert_eq!(
|
||||
store
|
||||
.active(&crate::config::AppProperties {
|
||||
title: None,
|
||||
class: None,
|
||||
exec: None,
|
||||
})
|
||||
.label(),
|
||||
"default"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -18,3 +18,45 @@ macro_rules! merge {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn os_matches(os: &str) -> bool {
|
||||
match os {
|
||||
"macos" => cfg!(target_os = "macos"),
|
||||
"windows" => cfg!(target_os = "windows"),
|
||||
"linux" => cfg!(target_os = "linux"),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn os_matches_linux() {
|
||||
assert!(os_matches("linux"));
|
||||
assert!(!os_matches("windows"));
|
||||
assert!(!os_matches("macos"));
|
||||
assert!(!os_matches("invalid"));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn os_matches_macos() {
|
||||
assert!(os_matches("macos"));
|
||||
assert!(!os_matches("windows"));
|
||||
assert!(!os_matches("linux"));
|
||||
assert!(!os_matches("invalid"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn os_matches_windows() {
|
||||
assert!(os_matches("windows"));
|
||||
assert!(!os_matches("macos"));
|
||||
assert!(!os_matches("linux"));
|
||||
assert!(!os_matches("invalid"));
|
||||
}
|
||||
}
|
|
@ -23,40 +23,3 @@ mod util;
|
|||
mod config;
|
||||
mod matches;
|
||||
mod counter;
|
||||
|
||||
use std::path::Path;
|
||||
use anyhow::Result;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use thiserror::Error;
|
||||
use config::Config;
|
||||
|
||||
|
||||
pub struct ConfigSet {
|
||||
|
||||
}
|
||||
|
||||
impl ConfigSet {
|
||||
//fn active(&self, app: AppProperties) -> &'a Config {
|
||||
// TODO: using the app properties, check if any of the sub configs match or not. If not, return the default
|
||||
// Here a RegexSet might be very useful to efficiently match them.
|
||||
//}
|
||||
|
||||
//fn default(&self) -> &'a Config {}
|
||||
}
|
||||
|
||||
pub struct AppProperties<'a> {
|
||||
pub title: Option<&'a str>,
|
||||
pub class: Option<&'a str>,
|
||||
pub exec: Option<&'a str>,
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn todo() {
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user