Add config store base implementation and tests

This commit is contained in:
Federico Terzi 2021-03-08 16:36:16 +01:00
parent 0ca740914f
commit 7262727823
5 changed files with 444 additions and 50 deletions

View File

@ -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>,
}

View File

@ -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"),
},
));
}
}

View 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"
);
}
}

View File

@ -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"));
}
}

View File

@ -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() {
}
}