Making progress in the config parsing

This commit is contained in:
Federico Terzi 2021-02-26 22:33:33 +01:00
parent e26a04de67
commit 2283cedbd3
8 changed files with 413 additions and 31 deletions

75
Cargo.lock generated
View File

@ -210,7 +210,7 @@ checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595"
dependencies = [
"heck",
"proc-macro2",
"quote 1.0.8",
"quote 1.0.9",
"syn 1.0.60",
]
@ -235,6 +235,7 @@ dependencies = [
"log",
"serde",
"serde_yaml",
"tempdir",
"thiserror",
]
@ -300,6 +301,12 @@ dependencies = [
"widestring",
]
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]]
name = "getrandom"
version = "0.1.16"
@ -522,13 +529,50 @@ checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
[[package]]
name = "quote"
version = "1.0.8"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
dependencies = [
"fuchsia-cprng",
"libc",
"rand_core 0.3.1",
"rdrand",
"winapi",
]
[[package]]
name = "rand_core"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
dependencies = [
"rand_core 0.4.2",
]
[[package]]
name = "rand_core"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
[[package]]
name = "rdrand"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "redox_syscall"
version = "0.1.57"
@ -564,6 +608,15 @@ version = "0.6.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581"
[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi",
]
[[package]]
name = "rust-argon2"
version = "0.8.3"
@ -604,7 +657,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31"
dependencies = [
"proc-macro2",
"quote 1.0.8",
"quote 1.0.9",
"syn 1.0.60",
]
@ -676,7 +729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081"
dependencies = [
"proc-macro2",
"quote 1.0.8",
"quote 1.0.9",
"unicode-xid 0.2.1",
]
@ -689,6 +742,16 @@ dependencies = [
"unicode-xid 0.0.4",
]
[[package]]
name = "tempdir"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
dependencies = [
"rand",
"remove_dir_all",
]
[[package]]
name = "termcolor"
version = "1.1.2"
@ -714,7 +777,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
dependencies = [
"proc-macro2",
"quote 1.0.8",
"quote 1.0.9",
"syn 1.0.60",
]

View File

@ -10,4 +10,7 @@ anyhow = "1.0.38"
thiserror = "1.0.23"
serde = { version = "1.0.123", features = ["derive"] }
serde_yaml = "0.8.17"
glob = "0.3.0"
glob = "0.3.0"
[dev-dependencies]
tempdir = "0.3.7"

View File

@ -0,0 +1,20 @@
#[macro_export]
macro_rules! merge {
( $t:ident, $child:expr, $parent:expr, $( $x:ident ),* ) => {
{
$(
if $child.$x.is_none() {
$child.$x = $parent.$x.clone();
}
)*
// Build a temporary object to verify that all fields
// are being used at compile time
$t {
$(
$x: None,
)*
};
}
};
}

View File

@ -4,6 +4,7 @@ use anyhow::Result;
mod yaml;
mod path;
mod macro_util;
pub struct Config {
pub label: Option<String>,

View File

@ -3,7 +3,6 @@ use std::collections::HashSet;
use glob::glob;
use log::error;
// TODO: test
pub fn calculate_paths<'a>(glob_patterns: impl Iterator<Item = &'a String>) -> HashSet<String> {
let mut path_set = HashSet::new();
for glob_pattern in glob_patterns {
@ -15,9 +14,10 @@ pub fn calculate_paths<'a>(glob_patterns: impl Iterator<Item = &'a String>) -> H
Ok(path) => {
path_set.insert(path.to_string_lossy().to_string());
}
Err(err) => {
error!("glob error when processing pattern: {}, with error: {}", glob_pattern, err)
}
Err(err) => error!(
"glob error when processing pattern: {}, with error: {}",
glob_pattern, err
),
}
}
}
@ -32,3 +32,41 @@ pub fn calculate_paths<'a>(glob_patterns: impl Iterator<Item = &'a String>) -> H
path_set
}
#[cfg(test)]
mod tests {
use std::fs::create_dir_all;
use super::*;
use tempdir::TempDir;
#[test]
fn calculate_paths_works_correctly() {
let dir = TempDir::new("tempconfig").unwrap();
let match_dir = dir.path().join("match");
create_dir_all(&match_dir).unwrap();
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 result = calculate_paths(vec![
format!("{}/**/*.yml", dir.path().to_string_lossy()),
format!("{}/match/sub/*.yml", dir.path().to_string_lossy()),
// Invalid path
"invalid".to_string(),
].iter());
let mut expected = HashSet::new();
expected.insert(format!("{}/match/base.yml", dir.path().to_string_lossy()));
expected.insert(format!("{}/match/another.yml", dir.path().to_string_lossy()));
expected.insert(format!("{}/match/_sub.yml", dir.path().to_string_lossy()));
expected.insert(format!("{}/match/sub/sub.yml", dir.path().to_string_lossy()));
assert_eq!(result, expected);
}
}

View File

@ -1,67 +1,125 @@
use std::collections::HashSet;
use anyhow::{private::kind::TraitKind, Result};
use serde::{Deserialize, Serialize};
use std::iter::FromIterator;
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, Clone)]
#[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>>,
pub use_standard_includes: bool,
#[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 {
// TODO test
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 {
STANDARD_INCLUDES.iter().for_each(|include| { includes.insert(include.to_string()); })
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()); })
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()); })
extra_includes.iter().for_each(|include| {
includes.insert(include.to_string());
})
}
includes
}
// TODO test
pub fn aggregate_excludes(&self) -> HashSet<String> {
let mut excludes = HashSet::new();
if self.use_standard_includes {
STANDARD_EXCLUDES.iter().for_each(|exclude| { excludes.insert(exclude.to_string()); })
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()); })
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()); })
extra_excludes.iter().for_each(|exclude| {
excludes.insert(exclude.to_string());
})
}
excludes
@ -69,21 +127,181 @@ impl YAMLConfig {
}
// TODO: convert to TryFrom (check the matches section for an example)
impl From<YAMLConfig> for super::Config {
impl TryFrom<YAMLConfig> for super::Config {
type Error = anyhow::Error;
// TODO: test
fn from(yaml_config: YAMLConfig) -> Self {
fn try_from(yaml_config: YAMLConfig) -> Result<Self, Self::Error> {
let includes = yaml_config.aggregate_includes();
let excludes = yaml_config.aggregate_excludes();
// Extract the paths
let exclude_paths = calculate_paths(excludes.iter());
let include_paths = calculate_paths(includes.iter());
let match_files: Vec<String> = Vec::from_iter(include_paths.difference(&exclude_paths).cloned());
Self {
let match_files: Vec<String> =
Vec::from_iter(include_paths.difference(&exclude_paths).cloned());
Ok(Self {
label: yaml_config.label,
match_files,
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use std::iter::FromIterator;
use std::{convert::TryInto, fs::create_dir_all};
use tempdir::TempDir;
// fn create_config(yaml: &str) -> Result<Config> {
// let yaml_config: YAMLConfig = serde_yaml::from_str(yaml)?;
// let m: Config = yaml_config.try_into()?;
// Ok(m)
// }
#[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));
}
// TODO: test conversion to Config (we need to test that the file match resolution works)
}

View File

@ -17,6 +17,7 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
mod util;
mod config;
mod matches;

View File

@ -0,0 +1,38 @@
/// Check if the given string represents an empty YAML.
/// In other words, it checks if the document is only composed
/// of spaces and/or comments
pub fn is_yaml_empty(yaml: &str) -> bool {
for line in yaml.lines() {
let trimmed_line = line.trim();
if !trimmed_line.starts_with("#") && !trimmed_line.is_empty() {
return false
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_yaml_empty_document_empty() {
assert_eq!(is_yaml_empty(""), true);
}
#[test]
fn is_yaml_empty_document_with_comments() {
assert_eq!(is_yaml_empty("\n#comment \n \n"), true);
}
#[test]
fn is_yaml_empty_document_with_comments_and_content() {
assert_eq!(is_yaml_empty("\n#comment \n field: true\n"), false);
}
#[test]
fn is_yaml_empty_document_with_content() {
assert_eq!(is_yaml_empty("\nfield: true\n"), false);
}
}