Improve the config parsing logic and test cases
This commit is contained in:
parent
7b9e43ab06
commit
0ca740914f
|
@ -1,16 +1,11 @@
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
|
|
||||||
mod yaml;
|
|
||||||
mod path;
|
mod path;
|
||||||
|
mod parse;
|
||||||
mod util;
|
mod util;
|
||||||
|
mod resolve;
|
||||||
|
|
||||||
pub struct Config {
|
pub trait Config {
|
||||||
pub label: Option<String>,
|
fn label(&self) -> &str;
|
||||||
//pub backend:
|
fn match_paths(&self) -> &HashSet<String>;
|
||||||
pub match_paths: HashSet<String>,
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
}
|
|
43
espanso-config/src/config/parse/mod.rs
Normal file
43
espanso-config/src/config/parse/mod.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use thiserror::Error;
|
||||||
|
use std::{convert::TryInto, path::Path};
|
||||||
|
|
||||||
|
mod yaml;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
|
pub(crate) struct ParsedConfig {
|
||||||
|
pub label: Option<String>,
|
||||||
|
|
||||||
|
// Includes
|
||||||
|
pub includes: Option<Vec<String>>,
|
||||||
|
pub excludes: Option<Vec<String>>,
|
||||||
|
pub extra_includes: Option<Vec<String>>,
|
||||||
|
pub extra_excludes: Option<Vec<String>>,
|
||||||
|
pub use_standard_includes: Option<bool>,
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
pub filter_title: Option<String>,
|
||||||
|
pub filter_class: Option<String>,
|
||||||
|
pub filter_exec: Option<String>,
|
||||||
|
pub filter_os: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParsedConfig {
|
||||||
|
pub fn load(path: &Path) -> Result<Self> {
|
||||||
|
let content = std::fs::read_to_string(path)?;
|
||||||
|
match yaml::YAMLConfig::parse_from_str(&content) {
|
||||||
|
Ok(config) => {
|
||||||
|
Ok(config.try_into()?)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
Err(ParsedConfigError::LoadFailed(err).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ParsedConfigError {
|
||||||
|
#[error("can't load config `{0}`")]
|
||||||
|
LoadFailed(#[from] anyhow::Error),
|
||||||
|
}
|
120
espanso-config/src/config/parse/yaml.rs
Normal file
120
espanso-config/src/config/parse/yaml.rs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
use crate::util::is_yaml_empty;
|
||||||
|
|
||||||
|
use super::ParsedConfig;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub(crate) struct YAMLConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
pub label: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub includes: Option<Vec<String>>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub excludes: Option<Vec<String>>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub extra_includes: Option<Vec<String>>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub extra_excludes: Option<Vec<String>>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub use_standard_includes: Option<bool>,
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
#[serde(default)]
|
||||||
|
pub filter_title: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub filter_class: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub filter_exec: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub filter_os: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YAMLConfig {
|
||||||
|
pub fn parse_from_str(yaml: &str) -> Result<Self> {
|
||||||
|
// Because an empty string is not valid YAML but we want to support it anyway
|
||||||
|
if is_yaml_empty(yaml) {
|
||||||
|
return Ok(serde_yaml::from_str(
|
||||||
|
"arbitrary_field_that_will_not_block_the_parser: true",
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(serde_yaml::from_str(yaml)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<YAMLConfig> for ParsedConfig {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(yaml_config: YAMLConfig) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
label: yaml_config.label,
|
||||||
|
|
||||||
|
use_standard_includes: yaml_config.use_standard_includes,
|
||||||
|
includes: yaml_config.includes,
|
||||||
|
extra_includes: yaml_config.extra_includes,
|
||||||
|
excludes: yaml_config.excludes,
|
||||||
|
extra_excludes: yaml_config.extra_excludes,
|
||||||
|
|
||||||
|
filter_class: yaml_config.filter_class,
|
||||||
|
filter_exec: yaml_config.filter_exec,
|
||||||
|
filter_os: yaml_config.filter_os,
|
||||||
|
filter_title: yaml_config.filter_title,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn conversion_to_parsed_config_works_correctly() {
|
||||||
|
let config = YAMLConfig::parse_from_str(
|
||||||
|
r#"
|
||||||
|
label: "test"
|
||||||
|
|
||||||
|
use_standard_includes: true
|
||||||
|
includes: ["test1"]
|
||||||
|
extra_includes: ["test2"]
|
||||||
|
excludes: ["test3"]
|
||||||
|
extra_excludes: ["test4"]
|
||||||
|
|
||||||
|
filter_class: "test5"
|
||||||
|
filter_exec: "test6"
|
||||||
|
filter_os: "test7"
|
||||||
|
filter_title: "test8"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let parsed_config: ParsedConfig = config.try_into().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parsed_config,
|
||||||
|
ParsedConfig {
|
||||||
|
label: Some("test".to_string()),
|
||||||
|
use_standard_includes: Some(true),
|
||||||
|
includes: Some(vec!["test1".to_string()]),
|
||||||
|
extra_includes: Some(vec!["test2".to_string()]),
|
||||||
|
excludes: Some(vec!["test3".to_string()]),
|
||||||
|
extra_excludes: Some(vec!["test4".to_string()]),
|
||||||
|
|
||||||
|
filter_class: Some("test5".to_string()),
|
||||||
|
filter_exec: Some("test6".to_string()),
|
||||||
|
filter_os: Some("test7".to_string()),
|
||||||
|
filter_title: Some("test8".to_string()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
path::{Path, PathBuf},
|
path::{Path},
|
||||||
};
|
};
|
||||||
|
|
||||||
use glob::glob;
|
use glob::glob;
|
||||||
|
|
393
espanso-config/src/config/resolve.rs
Normal file
393
espanso-config/src/config/resolve.rs
Normal file
|
@ -0,0 +1,393 @@
|
||||||
|
use super::{parse::ParsedConfig, path::calculate_paths, Config};
|
||||||
|
use crate::merge;
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
use std::{collections::HashSet, path::Path};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
const STANDARD_INCLUDES: &[&str] = &["../match/**/*.yml"];
|
||||||
|
const STANDARD_EXCLUDES: &[&str] = &["../match/**/_*.yml"];
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub(crate) struct ResolvedConfig {
|
||||||
|
parsed: ParsedConfig,
|
||||||
|
|
||||||
|
// Generated properties
|
||||||
|
match_paths: HashSet<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ResolvedConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
parsed: Default::default(),
|
||||||
|
match_paths: HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config for ResolvedConfig {
|
||||||
|
fn label(&self) -> &str {
|
||||||
|
self.parsed.label.as_deref().unwrap_or("none")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_paths(&self) -> &HashSet<String> {
|
||||||
|
&self.match_paths
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolvedConfig {
|
||||||
|
pub fn load(path: &Path, parent: Option<&Self>) -> Result<Self> {
|
||||||
|
let mut config = ParsedConfig::load(path)?;
|
||||||
|
|
||||||
|
// Merge with parent config if present
|
||||||
|
if let Some(parent) = parent {
|
||||||
|
Self::merge_parsed(&mut config, &parent.parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the base directory
|
||||||
|
let base_dir = path
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| ResolveError::ParentResolveFailed())?;
|
||||||
|
|
||||||
|
let match_paths = Self::generate_match_paths(&config, base_dir);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
parsed: config,
|
||||||
|
match_paths,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_parsed(child: &mut ParsedConfig, parent: &ParsedConfig) {
|
||||||
|
// Override the None fields with the parent's value
|
||||||
|
merge!(
|
||||||
|
ParsedConfig,
|
||||||
|
child,
|
||||||
|
parent,
|
||||||
|
// Fields
|
||||||
|
label,
|
||||||
|
includes,
|
||||||
|
excludes,
|
||||||
|
extra_includes,
|
||||||
|
extra_excludes,
|
||||||
|
use_standard_includes,
|
||||||
|
filter_title,
|
||||||
|
filter_class,
|
||||||
|
filter_exec,
|
||||||
|
filter_os
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aggregate_includes(config: &ParsedConfig) -> HashSet<String> {
|
||||||
|
let mut includes = HashSet::new();
|
||||||
|
|
||||||
|
if config.use_standard_includes.is_none() || config.use_standard_includes.unwrap() {
|
||||||
|
STANDARD_INCLUDES.iter().for_each(|include| {
|
||||||
|
includes.insert(include.to_string());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(yaml_includes) = config.includes.as_ref() {
|
||||||
|
yaml_includes.iter().for_each(|include| {
|
||||||
|
includes.insert(include.to_string());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(extra_includes) = config.extra_includes.as_ref() {
|
||||||
|
extra_includes.iter().for_each(|include| {
|
||||||
|
includes.insert(include.to_string());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
includes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aggregate_excludes(config: &ParsedConfig) -> HashSet<String> {
|
||||||
|
let mut excludes = HashSet::new();
|
||||||
|
|
||||||
|
if config.use_standard_includes.is_none() || config.use_standard_includes.unwrap() {
|
||||||
|
STANDARD_EXCLUDES.iter().for_each(|exclude| {
|
||||||
|
excludes.insert(exclude.to_string());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(yaml_excludes) = config.excludes.as_ref() {
|
||||||
|
yaml_excludes.iter().for_each(|exclude| {
|
||||||
|
excludes.insert(exclude.to_string());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(extra_excludes) = config.extra_excludes.as_ref() {
|
||||||
|
extra_excludes.iter().for_each(|exclude| {
|
||||||
|
excludes.insert(exclude.to_string());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
excludes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_match_paths(config: &ParsedConfig, base_dir: &Path) -> HashSet<String> {
|
||||||
|
let includes = Self::aggregate_includes(config);
|
||||||
|
let excludes = Self::aggregate_excludes(config);
|
||||||
|
|
||||||
|
// Extract the paths
|
||||||
|
let exclude_paths = calculate_paths(base_dir, excludes.iter());
|
||||||
|
let include_paths = calculate_paths(base_dir, includes.iter());
|
||||||
|
|
||||||
|
HashSet::from_iter(include_paths.difference(&exclude_paths).cloned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ResolveError {
|
||||||
|
#[error("unable to resolve parent path")]
|
||||||
|
ParentResolveFailed(),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::util::tests::use_test_directory;
|
||||||
|
use std::fs::create_dir_all;
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aggregate_includes_empty_config() {
|
||||||
|
assert_eq!(
|
||||||
|
ResolvedConfig::aggregate_includes(&ParsedConfig {
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
HashSet::from_iter(vec!["../match/**/*.yml".to_string(),].iter().cloned())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aggregate_includes_no_standard() {
|
||||||
|
assert_eq!(
|
||||||
|
ResolvedConfig::aggregate_includes(&ParsedConfig {
|
||||||
|
use_standard_includes: Some(false),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
HashSet::new()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aggregate_includes_custom_includes() {
|
||||||
|
assert_eq!(
|
||||||
|
ResolvedConfig::aggregate_includes(&ParsedConfig {
|
||||||
|
includes: Some(vec!["custom/*.yml".to_string()]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
HashSet::from_iter(
|
||||||
|
vec!["../match/**/*.yml".to_string(), "custom/*.yml".to_string()]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aggregate_includes_extra_includes() {
|
||||||
|
assert_eq!(
|
||||||
|
ResolvedConfig::aggregate_includes(&ParsedConfig {
|
||||||
|
extra_includes: Some(vec!["custom/*.yml".to_string()]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
HashSet::from_iter(
|
||||||
|
vec!["../match/**/*.yml".to_string(), "custom/*.yml".to_string()]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aggregate_includes_includes_and_extra_includes() {
|
||||||
|
assert_eq!(
|
||||||
|
ResolvedConfig::aggregate_includes(&ParsedConfig {
|
||||||
|
includes: Some(vec!["sub/*.yml".to_string()]),
|
||||||
|
extra_includes: Some(vec!["custom/*.yml".to_string()]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
HashSet::from_iter(
|
||||||
|
vec!["../match/**/*.yml".to_string(), "custom/*.yml".to_string(), "sub/*.yml".to_string()]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aggregate_excludes_empty_config() {
|
||||||
|
assert_eq!(
|
||||||
|
ResolvedConfig::aggregate_excludes(&ParsedConfig {
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
HashSet::from_iter(vec!["../match/**/_*.yml".to_string(),].iter().cloned())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aggregate_excludes_no_standard() {
|
||||||
|
assert_eq!(
|
||||||
|
ResolvedConfig::aggregate_excludes(&ParsedConfig {
|
||||||
|
use_standard_includes: Some(false),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
HashSet::new()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aggregate_excludes_custom_excludes() {
|
||||||
|
assert_eq!(
|
||||||
|
ResolvedConfig::aggregate_excludes(&ParsedConfig {
|
||||||
|
excludes: Some(vec!["custom/*.yml".to_string()]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
HashSet::from_iter(
|
||||||
|
vec!["../match/**/_*.yml".to_string(), "custom/*.yml".to_string()]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aggregate_excludes_extra_excludes() {
|
||||||
|
assert_eq!(
|
||||||
|
ResolvedConfig::aggregate_excludes(&ParsedConfig {
|
||||||
|
extra_excludes: Some(vec!["custom/*.yml".to_string()]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
HashSet::from_iter(
|
||||||
|
vec!["../match/**/_*.yml".to_string(), "custom/*.yml".to_string()]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aggregate_excludes_excludes_and_extra_excludes() {
|
||||||
|
assert_eq!(
|
||||||
|
ResolvedConfig::aggregate_excludes(&ParsedConfig {
|
||||||
|
excludes: Some(vec!["sub/*.yml".to_string()]),
|
||||||
|
extra_excludes: Some(vec!["custom/*.yml".to_string()]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
HashSet::from_iter(
|
||||||
|
vec!["../match/**/_*.yml".to_string(), "custom/*.yml".to_string(), "sub/*.yml".to_string()]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_parent_field_parent_fallback() {
|
||||||
|
let parent = ParsedConfig {
|
||||||
|
use_standard_includes: Some(false),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut child = ParsedConfig {
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert_eq!(child.use_standard_includes, None);
|
||||||
|
|
||||||
|
ResolvedConfig::merge_parsed(&mut child, &parent);
|
||||||
|
assert_eq!(child.use_standard_includes, Some(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_parent_field_child_overwrite_parent() {
|
||||||
|
let parent = ParsedConfig {
|
||||||
|
use_standard_includes: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut child = ParsedConfig {
|
||||||
|
use_standard_includes: Some(false),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert_eq!(child.use_standard_includes, Some(false));
|
||||||
|
|
||||||
|
ResolvedConfig::merge_parsed(&mut child, &parent);
|
||||||
|
assert_eq!(child.use_standard_includes, Some(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn match_paths_generated_correctly() {
|
||||||
|
use_test_directory(|_, match_dir, config_dir| {
|
||||||
|
let sub_dir = match_dir.join("sub");
|
||||||
|
create_dir_all(&sub_dir).unwrap();
|
||||||
|
|
||||||
|
let base_file = match_dir.join("base.yml");
|
||||||
|
std::fs::write(&base_file, "test").unwrap();
|
||||||
|
let another_file = match_dir.join("another.yml");
|
||||||
|
std::fs::write(&another_file, "test").unwrap();
|
||||||
|
let under_file = match_dir.join("_sub.yml");
|
||||||
|
std::fs::write(&under_file, "test").unwrap();
|
||||||
|
let sub_file = sub_dir.join("sub.yml");
|
||||||
|
std::fs::write(&sub_file, "test").unwrap();
|
||||||
|
|
||||||
|
let config_file = config_dir.join("default.yml");
|
||||||
|
std::fs::write(&config_file, "").unwrap();
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
assert_eq!(config.match_paths(), &expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn match_paths_generated_correctly_with_child_config() {
|
||||||
|
use_test_directory(|_, match_dir, config_dir| {
|
||||||
|
let sub_dir = match_dir.join("sub");
|
||||||
|
create_dir_all(&sub_dir).unwrap();
|
||||||
|
|
||||||
|
let base_file = match_dir.join("base.yml");
|
||||||
|
std::fs::write(&base_file, "test").unwrap();
|
||||||
|
let another_file = match_dir.join("another.yml");
|
||||||
|
std::fs::write(&another_file, "test").unwrap();
|
||||||
|
let under_file = match_dir.join("_sub.yml");
|
||||||
|
std::fs::write(&under_file, "test").unwrap();
|
||||||
|
let sub_file = sub_dir.join("another.yml");
|
||||||
|
std::fs::write(&sub_file, "test").unwrap();
|
||||||
|
let sub_under_file = sub_dir.join("_sub.yml");
|
||||||
|
std::fs::write(&sub_under_file, "test").unwrap();
|
||||||
|
|
||||||
|
// Configs
|
||||||
|
|
||||||
|
let parent_file = config_dir.join("parent.yml");
|
||||||
|
std::fs::write(&parent_file, r#"
|
||||||
|
excludes: ['../**/another.yml']
|
||||||
|
"#).unwrap();
|
||||||
|
|
||||||
|
let config_file = config_dir.join("default.yml");
|
||||||
|
std::fs::write(&config_file, r#"
|
||||||
|
use_standard_includes: false
|
||||||
|
excludes: []
|
||||||
|
includes: ["../match/sub/*.yml"]
|
||||||
|
"#).unwrap();
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
assert_eq!(child.match_paths(), &expected);
|
||||||
|
|
||||||
|
let mut expected = HashSet::new();
|
||||||
|
expected.insert(base_file.to_string_lossy().to_string());
|
||||||
|
|
||||||
|
assert_eq!(parent.match_paths(), &expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,355 +0,0 @@
|
||||||
use anyhow::{private::kind::TraitKind, Result};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{iter::FromIterator, path::Path};
|
|
||||||
use std::{collections::HashSet, convert::TryFrom};
|
|
||||||
|
|
||||||
use crate::{merge, util::is_yaml_empty};
|
|
||||||
|
|
||||||
use super::path::calculate_paths;
|
|
||||||
|
|
||||||
const STANDARD_INCLUDES: &[&str] = &["match/**/*.yml"];
|
|
||||||
const STANDARD_EXCLUDES: &[&str] = &["match/**/_*.yml"];
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct YAMLConfig {
|
|
||||||
#[serde(default)]
|
|
||||||
pub label: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub includes: Option<Vec<String>>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub excludes: Option<Vec<String>>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub extra_includes: Option<Vec<String>>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub extra_excludes: Option<Vec<String>>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub use_standard_includes: Option<bool>,
|
|
||||||
|
|
||||||
// Filters
|
|
||||||
#[serde(default)]
|
|
||||||
pub filter_title: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub filter_class: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub filter_exec: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub filter_os: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl YAMLConfig {
|
|
||||||
pub fn parse_from_str(yaml: &str) -> Result<Self> {
|
|
||||||
// Because an empty string is not valid YAML but we want to support it anyway
|
|
||||||
if is_yaml_empty(yaml) {
|
|
||||||
return Ok(serde_yaml::from_str(
|
|
||||||
"arbitrary_field_that_will_not_block_the_parser: true",
|
|
||||||
)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(serde_yaml::from_str(yaml)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn merge_parent(&mut self, parent: &YAMLConfig) {
|
|
||||||
// Override the None fields with the parent's value
|
|
||||||
merge!(
|
|
||||||
YAMLConfig,
|
|
||||||
self,
|
|
||||||
parent,
|
|
||||||
|
|
||||||
// Fields
|
|
||||||
label,
|
|
||||||
includes,
|
|
||||||
excludes,
|
|
||||||
extra_includes,
|
|
||||||
extra_excludes,
|
|
||||||
use_standard_includes,
|
|
||||||
filter_title,
|
|
||||||
filter_class,
|
|
||||||
filter_exec,
|
|
||||||
filter_os
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn aggregate_includes(&self) -> HashSet<String> {
|
|
||||||
let mut includes = HashSet::new();
|
|
||||||
|
|
||||||
if self.use_standard_includes.is_none() || self.use_standard_includes.unwrap() {
|
|
||||||
STANDARD_INCLUDES.iter().for_each(|include| {
|
|
||||||
includes.insert(include.to_string());
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(yaml_includes) = self.includes.as_ref() {
|
|
||||||
yaml_includes.iter().for_each(|include| {
|
|
||||||
includes.insert(include.to_string());
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(extra_includes) = self.extra_includes.as_ref() {
|
|
||||||
extra_includes.iter().for_each(|include| {
|
|
||||||
includes.insert(include.to_string());
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
includes
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn aggregate_excludes(&self) -> HashSet<String> {
|
|
||||||
let mut excludes = HashSet::new();
|
|
||||||
|
|
||||||
if self.use_standard_includes.is_none() || self.use_standard_includes.unwrap() {
|
|
||||||
STANDARD_EXCLUDES.iter().for_each(|exclude| {
|
|
||||||
excludes.insert(exclude.to_string());
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(yaml_excludes) = self.excludes.as_ref() {
|
|
||||||
yaml_excludes.iter().for_each(|exclude| {
|
|
||||||
excludes.insert(exclude.to_string());
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(extra_excludes) = self.extra_excludes.as_ref() {
|
|
||||||
extra_excludes.iter().for_each(|exclude| {
|
|
||||||
excludes.insert(exclude.to_string());
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
excludes
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_match_paths(&self, base_dir: &Path) -> HashSet<String> {
|
|
||||||
let includes = self.aggregate_includes();
|
|
||||||
let excludes = self.aggregate_excludes();
|
|
||||||
|
|
||||||
// Extract the paths
|
|
||||||
let exclude_paths = calculate_paths(base_dir, excludes.iter());
|
|
||||||
let include_paths = calculate_paths(base_dir, includes.iter());
|
|
||||||
|
|
||||||
HashSet::from_iter(include_paths.difference(&exclude_paths).cloned())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: test
|
|
||||||
pub fn to_config(&self, base_dir: &Path) -> Result<super::Config> {
|
|
||||||
let match_paths = self.generate_match_paths(base_dir);
|
|
||||||
|
|
||||||
Ok(super::Config {
|
|
||||||
label: self.label.clone(),
|
|
||||||
match_paths,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::util::tests::use_test_directory;
|
|
||||||
use std::iter::FromIterator;
|
|
||||||
use std::fs::create_dir_all;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aggregate_includes_empty_config() {
|
|
||||||
assert_eq!(
|
|
||||||
YAMLConfig::parse_from_str("").unwrap().aggregate_includes(),
|
|
||||||
HashSet::from_iter(vec!["match/**/*.yml".to_string(),].iter().cloned())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aggregate_includes_no_standard() {
|
|
||||||
assert_eq!(
|
|
||||||
YAMLConfig::parse_from_str("use_standard_includes: false").unwrap().aggregate_includes(),
|
|
||||||
HashSet::new()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aggregate_includes_custom_includes() {
|
|
||||||
assert_eq!(
|
|
||||||
YAMLConfig::parse_from_str("includes: ['custom/*.yml']")
|
|
||||||
.unwrap()
|
|
||||||
.aggregate_includes(),
|
|
||||||
HashSet::from_iter(
|
|
||||||
vec!["match/**/*.yml".to_string(), "custom/*.yml".to_string()]
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aggregate_includes_extra_includes() {
|
|
||||||
assert_eq!(
|
|
||||||
YAMLConfig::parse_from_str("extra_includes: ['custom/*.yml']")
|
|
||||||
.unwrap()
|
|
||||||
.aggregate_includes(),
|
|
||||||
HashSet::from_iter(
|
|
||||||
vec!["match/**/*.yml".to_string(), "custom/*.yml".to_string()]
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aggregate_includes_includes_and_extra_includes() {
|
|
||||||
assert_eq!(
|
|
||||||
YAMLConfig::parse_from_str("includes: ['sub/*.yml']\nextra_includes: ['custom/*.yml']")
|
|
||||||
.unwrap()
|
|
||||||
.aggregate_includes(),
|
|
||||||
HashSet::from_iter(
|
|
||||||
vec!["match/**/*.yml".to_string(), "custom/*.yml".to_string(), "sub/*.yml".to_string()]
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aggregate_excludes_empty_config() {
|
|
||||||
assert_eq!(
|
|
||||||
YAMLConfig::parse_from_str("").unwrap().aggregate_excludes(),
|
|
||||||
HashSet::from_iter(vec!["match/**/_*.yml".to_string(),].iter().cloned())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aggregate_excludes_no_standard() {
|
|
||||||
assert_eq!(
|
|
||||||
YAMLConfig::parse_from_str("use_standard_includes: false").unwrap().aggregate_excludes(),
|
|
||||||
HashSet::new()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aggregate_excludes_custom_excludes() {
|
|
||||||
assert_eq!(
|
|
||||||
YAMLConfig::parse_from_str("excludes: ['custom/*.yml']")
|
|
||||||
.unwrap()
|
|
||||||
.aggregate_excludes(),
|
|
||||||
HashSet::from_iter(
|
|
||||||
vec!["match/**/_*.yml".to_string(), "custom/*.yml".to_string()]
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aggregate_excludes_extra_excludes() {
|
|
||||||
assert_eq!(
|
|
||||||
YAMLConfig::parse_from_str("extra_excludes: ['custom/*.yml']")
|
|
||||||
.unwrap()
|
|
||||||
.aggregate_excludes(),
|
|
||||||
HashSet::from_iter(
|
|
||||||
vec!["match/**/_*.yml".to_string(), "custom/*.yml".to_string()]
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aggregate_excludes_excludes_and_extra_excludes() {
|
|
||||||
assert_eq!(
|
|
||||||
YAMLConfig::parse_from_str("excludes: ['sub/*.yml']\nextra_excludes: ['custom/*.yml']")
|
|
||||||
.unwrap()
|
|
||||||
.aggregate_excludes(),
|
|
||||||
HashSet::from_iter(
|
|
||||||
vec!["match/**/_*.yml".to_string(), "custom/*.yml".to_string(), "sub/*.yml".to_string()]
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn merge_parent_field_parent_fallback() {
|
|
||||||
let parent =
|
|
||||||
YAMLConfig::parse_from_str("use_standard_includes: false").unwrap();
|
|
||||||
let mut child =
|
|
||||||
YAMLConfig::parse_from_str("").unwrap();
|
|
||||||
assert_eq!(child.use_standard_includes, None);
|
|
||||||
|
|
||||||
child.merge_parent(&parent);
|
|
||||||
assert_eq!(child.use_standard_includes, Some(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn merge_parent_field_child_overwrite_parent() {
|
|
||||||
let parent =
|
|
||||||
YAMLConfig::parse_from_str("use_standard_includes: true").unwrap();
|
|
||||||
let mut child =
|
|
||||||
YAMLConfig::parse_from_str("use_standard_includes: false").unwrap();
|
|
||||||
assert_eq!(child.use_standard_includes, Some(false));
|
|
||||||
|
|
||||||
child.merge_parent(&parent);
|
|
||||||
assert_eq!(child.use_standard_includes, Some(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn generate_match_paths_works_correctly() {
|
|
||||||
use_test_directory(|base, match_dir, _| {
|
|
||||||
let sub_dir = match_dir.join("sub");
|
|
||||||
create_dir_all(&sub_dir).unwrap();
|
|
||||||
|
|
||||||
std::fs::write(match_dir.join("base.yml"), "test").unwrap();
|
|
||||||
std::fs::write(match_dir.join("another.yml"), "test").unwrap();
|
|
||||||
std::fs::write(match_dir.join("_sub.yml"), "test").unwrap();
|
|
||||||
std::fs::write(sub_dir.join("sub.yml"), "test").unwrap();
|
|
||||||
|
|
||||||
let config = YAMLConfig::parse_from_str("").unwrap();
|
|
||||||
|
|
||||||
let mut expected = HashSet::new();
|
|
||||||
expected.insert(format!("{}/match/base.yml", base.to_string_lossy()));
|
|
||||||
expected.insert(format!("{}/match/another.yml", base.to_string_lossy()));
|
|
||||||
expected.insert(format!("{}/match/sub/sub.yml", base.to_string_lossy()));
|
|
||||||
|
|
||||||
assert_eq!(config.generate_match_paths(base), expected);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn generate_match_paths_works_correctly_with_child_config() {
|
|
||||||
use_test_directory(|base, match_dir, _| {
|
|
||||||
let sub_dir = match_dir.join("sub");
|
|
||||||
create_dir_all(&sub_dir).unwrap();
|
|
||||||
|
|
||||||
std::fs::write(match_dir.join("base.yml"), "test").unwrap();
|
|
||||||
std::fs::write(match_dir.join("another.yml"), "test").unwrap();
|
|
||||||
std::fs::write(match_dir.join("_sub.yml"), "test").unwrap();
|
|
||||||
std::fs::write(sub_dir.join("another.yml"), "test").unwrap();
|
|
||||||
std::fs::write(sub_dir.join("_sub.yml"), "test").unwrap();
|
|
||||||
|
|
||||||
let parent = YAMLConfig::parse_from_str(r"
|
|
||||||
excludes: ['**/another.yml']
|
|
||||||
").unwrap();
|
|
||||||
let mut child = YAMLConfig::parse_from_str(r"
|
|
||||||
use_standard_includes: false
|
|
||||||
excludes: []
|
|
||||||
includes: ['match/sub/*.yml']
|
|
||||||
").unwrap();
|
|
||||||
child.merge_parent(&parent);
|
|
||||||
|
|
||||||
let mut expected = HashSet::new();
|
|
||||||
expected.insert(format!("{}/match/sub/another.yml", base.to_string_lossy()));
|
|
||||||
expected.insert(format!("{}/match/sub/_sub.yml", base.to_string_lossy()));
|
|
||||||
|
|
||||||
assert_eq!(child.generate_match_paths(base), expected);
|
|
||||||
|
|
||||||
let mut expected = HashSet::new();
|
|
||||||
expected.insert(format!("{}/match/base.yml", base.to_string_lossy()));
|
|
||||||
|
|
||||||
assert_eq!(parent.generate_match_paths(base), expected);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: test conversion to Config (we need to test that the file match resolution works)
|
|
||||||
}
|
|
|
@ -91,9 +91,8 @@ mod tests {
|
||||||
server.accept_one().unwrap();
|
server.accept_one().unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
if cfg!(target_os = "windows") {
|
// TODO: avoid delay and change the IPC code so that we can wait for the IPC
|
||||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
//std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
}
|
|
||||||
|
|
||||||
let client = client::<Event>("testespansoipc", &std::env::temp_dir()).unwrap();
|
let client = client::<Event>("testespansoipc", &std::env::temp_dir()).unwrap();
|
||||||
client.send(Event::Foo("hello".to_string())).unwrap();
|
client.send(Event::Foo("hello".to_string())).unwrap();
|
||||||
|
|
Loading…
Reference in New Issue
Block a user