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 anyhow::Result;
|
||||
|
||||
|
||||
mod yaml;
|
||||
mod path;
|
||||
mod parse;
|
||||
mod util;
|
||||
mod resolve;
|
||||
|
||||
pub struct Config {
|
||||
pub label: Option<String>,
|
||||
//pub backend:
|
||||
pub match_paths: HashSet<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
}
|
||||
pub trait Config {
|
||||
fn label(&self) -> &str;
|
||||
fn match_paths(&self) -> &HashSet<String>;
|
||||
}
|
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::{
|
||||
collections::HashSet,
|
||||
path::{Path, PathBuf},
|
||||
path::{Path},
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
}
|
||||
// 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));
|
||||
|
||||
let client = client::<Event>("testespansoipc", &std::env::temp_dir()).unwrap();
|
||||
client.send(Event::Foo("hello".to_string())).unwrap();
|
||||
|
|
Loading…
Reference in New Issue
Block a user