Refactor config structure and improve importer logic
This commit is contained in:
parent
2cb8da91a5
commit
3974d90bc9
|
@ -4,7 +4,7 @@ use anyhow::Result;
|
||||||
|
|
||||||
mod yaml;
|
mod yaml;
|
||||||
mod path;
|
mod path;
|
||||||
mod macro_util;
|
mod util;
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
|
|
|
@ -47,21 +47,9 @@ pub fn calculate_paths<'a>(base_dir: &Path, glob_patterns: impl Iterator<Item =
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
use std::{fs::create_dir_all, path::Path};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use tempdir::TempDir;
|
use crate::util::tests::use_test_directory;
|
||||||
|
use std::{fs::create_dir_all};
|
||||||
pub fn use_test_directory(callback: impl FnOnce(&Path, &Path, &Path)) {
|
|
||||||
let dir = TempDir::new("tempconfig").unwrap();
|
|
||||||
let match_dir = dir.path().join("match");
|
|
||||||
create_dir_all(&match_dir).unwrap();
|
|
||||||
|
|
||||||
let config_dir = dir.path().join("config");
|
|
||||||
create_dir_all(&config_dir).unwrap();
|
|
||||||
|
|
||||||
callback(&dir.path(), &match_dir, &config_dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn calculate_paths_relative_paths() {
|
fn calculate_paths_relative_paths() {
|
||||||
|
|
|
@ -150,10 +150,9 @@ impl YAMLConfig {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::Config;
|
use crate::util::tests::use_test_directory;
|
||||||
use crate::config::path::tests::use_test_directory;
|
|
||||||
use std::iter::FromIterator;
|
use std::iter::FromIterator;
|
||||||
use std::{convert::TryInto, fs::create_dir_all};
|
use std::fs::create_dir_all;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn aggregate_includes_empty_config() {
|
fn aggregate_includes_empty_config() {
|
||||||
|
|
130
espanso-config/src/matches/group/loader/mod.rs
Normal file
130
espanso-config/src/matches/group/loader/mod.rs
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::path::Path;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use self::yaml::YAMLImporter;
|
||||||
|
|
||||||
|
use super::MatchGroup;
|
||||||
|
|
||||||
|
mod yaml;
|
||||||
|
|
||||||
|
trait Importer {
|
||||||
|
fn is_supported(&self, extension: &str) -> bool;
|
||||||
|
fn load_group(&self, path: &Path) -> Result<MatchGroup>;
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref IMPORTERS: Vec<Box<dyn Importer + Sync + Send>> = vec![
|
||||||
|
Box::new(YAMLImporter::new()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn load_match_group(path: &Path) -> Result<MatchGroup> {
|
||||||
|
if let Some(extension) = path.extension() {
|
||||||
|
let extension = extension.to_string_lossy().to_lowercase();
|
||||||
|
|
||||||
|
let importer = IMPORTERS
|
||||||
|
.iter()
|
||||||
|
.find(|importer| importer.is_supported(&extension));
|
||||||
|
|
||||||
|
match importer {
|
||||||
|
Some(importer) => match importer.load_group(path) {
|
||||||
|
Ok(group) => Ok(group),
|
||||||
|
Err(err) => Err(LoadError::ParsingError(err).into()),
|
||||||
|
},
|
||||||
|
None => Err(LoadError::InvalidFormat().into()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(LoadError::MissingExtension().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum LoadError {
|
||||||
|
#[error("missing extension in match group file")]
|
||||||
|
MissingExtension(),
|
||||||
|
|
||||||
|
#[error("invalid match group format")]
|
||||||
|
InvalidFormat(),
|
||||||
|
|
||||||
|
#[error("parser reported an error: `{0}`")]
|
||||||
|
ParsingError(anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::util::tests::use_test_directory;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_group_invalid_format() {
|
||||||
|
use_test_directory(|_, match_dir, _| {
|
||||||
|
let file = match_dir.join("base.invalid");
|
||||||
|
std::fs::write(&file, "test").unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(load_match_group(&file).unwrap_err().downcast::<LoadError>().unwrap(), LoadError::InvalidFormat()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_group_missing_extension() {
|
||||||
|
use_test_directory(|_, match_dir, _| {
|
||||||
|
let file = match_dir.join("base");
|
||||||
|
std::fs::write(&file, "test").unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(load_match_group(&file).unwrap_err().downcast::<LoadError>().unwrap(), LoadError::MissingExtension()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_group_parsing_error() {
|
||||||
|
use_test_directory(|_, match_dir, _| {
|
||||||
|
let file = match_dir.join("base.yml");
|
||||||
|
std::fs::write(&file, "test").unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(load_match_group(&file).unwrap_err().downcast::<LoadError>().unwrap(), LoadError::ParsingError(_)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_group_yaml_format() {
|
||||||
|
use_test_directory(|_, match_dir, _| {
|
||||||
|
let file = match_dir.join("base.yml");
|
||||||
|
std::fs::write(&file, r#"
|
||||||
|
matches:
|
||||||
|
- trigger: "hello"
|
||||||
|
replace: "world"
|
||||||
|
"#).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(load_match_group(&file).unwrap().matches.len(), 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_group_yaml_format_2() {
|
||||||
|
use_test_directory(|_, match_dir, _| {
|
||||||
|
let file = match_dir.join("base.yaml");
|
||||||
|
std::fs::write(&file, r#"
|
||||||
|
matches:
|
||||||
|
- trigger: "hello"
|
||||||
|
replace: "world"
|
||||||
|
"#).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(load_match_group(&file).unwrap().matches.len(), 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_group_yaml_format_casing() {
|
||||||
|
use_test_directory(|_, match_dir, _| {
|
||||||
|
let file = match_dir.join("base.YML");
|
||||||
|
std::fs::write(&file, r#"
|
||||||
|
matches:
|
||||||
|
- trigger: "hello"
|
||||||
|
replace: "world"
|
||||||
|
"#).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(load_match_group(&file).unwrap().matches.len(), 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,52 +1,39 @@
|
||||||
use std::{collections::HashMap, convert::{TryFrom, TryInto}, path::Path};
|
use crate::matches::{Match, Variable, group::{MatchGroup, path::resolve_imports}};
|
||||||
|
use log::warn;
|
||||||
|
use parse::YAMLMatchGroup;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde::{Deserialize, Serialize};
|
use std::convert::{TryFrom, TryInto};
|
||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use crate::util::is_yaml_empty;
|
use self::parse::{YAMLMatch, YAMLVariable};
|
||||||
|
use crate::matches::{MatchCause, MatchEffect, TextEffect, TriggerCause};
|
||||||
|
|
||||||
use crate::matches::{Match, MatchCause, MatchEffect, TextEffect, TriggerCause, Variable};
|
use super::Importer;
|
||||||
use super::{MatchGroup};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
mod parse;
|
||||||
pub struct YAMLMatchGroup {
|
|
||||||
#[serde(default)]
|
|
||||||
pub imports: Option<Vec<String>>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
pub(crate) struct YAMLImporter {}
|
||||||
pub global_vars: Option<Vec<YAMLVariable>>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
impl YAMLImporter {
|
||||||
pub matches: Option<Vec<YAMLMatch>>,
|
pub fn new() -> Self {
|
||||||
}
|
Self {}
|
||||||
|
|
||||||
impl YAMLMatchGroup {
|
|
||||||
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)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: test
|
|
||||||
pub fn parse_from_file(path: &Path) -> Result<Self> {
|
|
||||||
let content = std::fs::read_to_string(path)?;
|
|
||||||
Self::parse_from_str(&content)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<YAMLMatchGroup> for MatchGroup {
|
impl Importer for YAMLImporter {
|
||||||
type Error = anyhow::Error;
|
fn is_supported(&self, extension: &str) -> bool {
|
||||||
|
extension == "yaml" || extension == "yml"
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: test
|
// TODO: test
|
||||||
fn try_from(yaml_match_group: YAMLMatchGroup) -> Result<Self, Self::Error> {
|
// TODO: test resolve imports
|
||||||
let global_vars: Result<Vec<Variable>> = yaml_match_group
|
// TODO: test cyclical dependency
|
||||||
|
fn load_group(
|
||||||
|
&self,
|
||||||
|
path: &std::path::Path,
|
||||||
|
) -> anyhow::Result<crate::matches::group::MatchGroup> {
|
||||||
|
let yaml_group = YAMLMatchGroup::parse_from_file(path)?;
|
||||||
|
|
||||||
|
let global_vars: Result<Vec<Variable>> = yaml_group
|
||||||
.global_vars
|
.global_vars
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.cloned()
|
.cloned()
|
||||||
|
@ -55,7 +42,7 @@ impl TryFrom<YAMLMatchGroup> for MatchGroup {
|
||||||
.map(|var| var.clone().try_into())
|
.map(|var| var.clone().try_into())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let matches: Result<Vec<Match>> = yaml_match_group
|
let matches: Result<Vec<Match>> = yaml_group
|
||||||
.matches
|
.matches
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.cloned()
|
.cloned()
|
||||||
|
@ -64,78 +51,17 @@ impl TryFrom<YAMLMatchGroup> for MatchGroup {
|
||||||
.map(|m| m.clone().try_into())
|
.map(|m| m.clone().try_into())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// Resolve imports
|
||||||
|
let resolved_imports = resolve_imports(path, &yaml_group.imports.unwrap_or_default())?;
|
||||||
|
|
||||||
Ok(MatchGroup {
|
Ok(MatchGroup {
|
||||||
imports: yaml_match_group.imports.unwrap_or_default(),
|
imports: resolved_imports,
|
||||||
global_vars: global_vars?,
|
global_vars: global_vars?,
|
||||||
matches: matches?,
|
matches: matches?,
|
||||||
..Default::default()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct YAMLMatch {
|
|
||||||
#[serde(default)]
|
|
||||||
pub trigger: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub triggers: Option<Vec<String>>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub replace: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub image_path: Option<String>, // TODO: map
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub form: Option<String>, // TODO: map
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub form_fields: Option<HashMap<String, Value>>, // TODO: map
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub vars: Option<Vec<YAMLVariable>>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub word: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub left_word: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub right_word: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub propagate_case: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub force_clipboard: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub markdown: Option<String>, // TODO: map
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub paragraph: Option<bool>, // TODO: map
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub html: Option<String>, // TODO: map
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub struct YAMLVariable {
|
|
||||||
pub name: String,
|
|
||||||
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub var_type: String,
|
|
||||||
|
|
||||||
#[serde(default = "default_params")]
|
|
||||||
pub params: Mapping,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_params() -> Mapping {
|
|
||||||
Mapping::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<YAMLMatch> for Match {
|
impl TryFrom<YAMLMatch> for Match {
|
||||||
type Error = anyhow::Error;
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
@ -183,7 +109,9 @@ impl TryFrom<YAMLMatch> for Match {
|
||||||
MatchEffect::None
|
MatchEffect::None
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: log none match effect
|
if let MatchEffect::None = effect {
|
||||||
|
warn!("match caused by {:?} does not produce any effect. Did you forget the 'replace' field?", cause);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
cause,
|
cause,
|
||||||
|
@ -208,15 +136,10 @@ impl TryFrom<YAMLVariable> for Variable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum YAMLConversionError {
|
|
||||||
// TODO
|
|
||||||
//#[error("unknown data store error")]
|
|
||||||
//Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use serde_yaml::{Mapping, Value};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::matches::Match;
|
use crate::matches::Match;
|
||||||
|
|
101
espanso-config/src/matches/group/loader/yaml/parse.rs
Normal file
101
espanso-config/src/matches/group/loader/yaml/parse.rs
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
use std::{collections::HashMap, path::Path};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_yaml::{Mapping, Value};
|
||||||
|
|
||||||
|
use crate::util::is_yaml_empty;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct YAMLMatchGroup {
|
||||||
|
#[serde(default)]
|
||||||
|
pub imports: Option<Vec<String>>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub global_vars: Option<Vec<YAMLVariable>>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub matches: Option<Vec<YAMLMatch>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YAMLMatchGroup {
|
||||||
|
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)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: test
|
||||||
|
pub fn parse_from_file(path: &Path) -> Result<Self> {
|
||||||
|
let content = std::fs::read_to_string(path)?;
|
||||||
|
Self::parse_from_str(&content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct YAMLMatch {
|
||||||
|
#[serde(default)]
|
||||||
|
pub trigger: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub triggers: Option<Vec<String>>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub replace: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub image_path: Option<String>, // TODO: map
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub form: Option<String>, // TODO: map
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub form_fields: Option<HashMap<String, Value>>, // TODO: map
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub vars: Option<Vec<YAMLVariable>>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub word: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub left_word: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub right_word: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub propagate_case: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub force_clipboard: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub markdown: Option<String>, // TODO: map
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub paragraph: Option<bool>, // TODO: map
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub html: Option<String>, // TODO: map
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||||
|
pub struct YAMLVariable {
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub var_type: String,
|
||||||
|
|
||||||
|
#[serde(default = "default_params")]
|
||||||
|
pub params: Mapping,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_params() -> Mapping {
|
||||||
|
Mapping::new()
|
||||||
|
}
|
|
@ -1,22 +1,18 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use log::error;
|
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
path::{Path},
|
||||||
convert::TryInto,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use super::{Match, Variable};
|
use super::{Match, Variable};
|
||||||
|
|
||||||
mod yaml;
|
mod loader;
|
||||||
|
mod path;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub(crate) struct MatchGroup {
|
pub(crate) struct MatchGroup {
|
||||||
imports: Vec<String>,
|
pub imports: Vec<String>,
|
||||||
pub global_vars: Vec<Variable>,
|
pub global_vars: Vec<Variable>,
|
||||||
pub matches: Vec<Match>,
|
pub matches: Vec<Match>,
|
||||||
|
|
||||||
pub resolved_imports: Vec<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MatchGroup {
|
impl Default for MatchGroup {
|
||||||
|
@ -25,7 +21,6 @@ impl Default for MatchGroup {
|
||||||
imports: Vec::new(),
|
imports: Vec::new(),
|
||||||
global_vars: Vec::new(),
|
global_vars: Vec::new(),
|
||||||
matches: Vec::new(),
|
matches: Vec::new(),
|
||||||
resolved_imports: Vec::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,101 +28,6 @@ impl Default for MatchGroup {
|
||||||
impl MatchGroup {
|
impl MatchGroup {
|
||||||
// TODO: test
|
// TODO: test
|
||||||
pub fn load(group_path: &Path) -> Result<Self> {
|
pub fn load(group_path: &Path) -> Result<Self> {
|
||||||
if let Some(extension) = group_path.extension() {
|
loader::load_match_group(group_path)
|
||||||
let extension = extension.to_string_lossy().to_lowercase();
|
|
||||||
|
|
||||||
if extension == "yml" || extension == "yaml" {
|
|
||||||
match yaml::YAMLMatchGroup::parse_from_file(group_path) {
|
|
||||||
Ok(yaml_group) => {
|
|
||||||
let match_group: Result<MatchGroup, _> = yaml_group.try_into();
|
|
||||||
match match_group {
|
|
||||||
Ok(mut group) => {
|
|
||||||
group.resolve_imports(group_path)?;
|
|
||||||
Ok(group)
|
|
||||||
}
|
|
||||||
Err(err) => Err(MatchGroupError::ParsingError(err).into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => Err(MatchGroupError::ParsingError(err).into()),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(MatchGroupError::InvalidFormat().into())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(MatchGroupError::MissingExtension().into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: test
|
|
||||||
fn resolve_imports(&mut self, group_path: &Path) -> Result<()> {
|
|
||||||
let mut paths = Vec::new();
|
|
||||||
|
|
||||||
if !group_path.exists() {
|
|
||||||
return Err(
|
|
||||||
MatchGroupError::ResolveImportFailed(format!(
|
|
||||||
"unable to resolve imports for match group at path: {:?}",
|
|
||||||
group_path
|
|
||||||
))
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the containing directory
|
|
||||||
let current_dir = if group_path.is_file() {
|
|
||||||
if let Some(parent) = group_path.parent() {
|
|
||||||
parent
|
|
||||||
} else {
|
|
||||||
return Err(
|
|
||||||
MatchGroupError::ResolveImportFailed(format!(
|
|
||||||
"unable to resolve imports for match group starting from current path: {:?}",
|
|
||||||
group_path
|
|
||||||
))
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
group_path
|
|
||||||
};
|
|
||||||
|
|
||||||
for import in self.imports.iter() {
|
|
||||||
let import_path = PathBuf::from(import);
|
|
||||||
|
|
||||||
// Absolute or relative import
|
|
||||||
let full_path = if import_path.is_relative() {
|
|
||||||
current_dir.join(import_path)
|
|
||||||
} else {
|
|
||||||
import_path
|
|
||||||
};
|
|
||||||
|
|
||||||
if full_path.exists() && full_path.is_file() {
|
|
||||||
paths.push(full_path)
|
|
||||||
} else {
|
|
||||||
// Best effort imports
|
|
||||||
error!("unable to resolve import at path: {:?}", full_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let string_paths = paths
|
|
||||||
.into_iter()
|
|
||||||
.map(|path| path.to_string_lossy().to_string())
|
|
||||||
.collect();
|
|
||||||
self.resolved_imports = string_paths;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum MatchGroupError {
|
|
||||||
#[error("missing extension in match group file")]
|
|
||||||
MissingExtension(),
|
|
||||||
|
|
||||||
#[error("invalid match group format")]
|
|
||||||
InvalidFormat(),
|
|
||||||
|
|
||||||
#[error("parser reported an error: `{0}`")]
|
|
||||||
ParsingError(anyhow::Error),
|
|
||||||
|
|
||||||
#[error("resolve import failed: `{0}`")]
|
|
||||||
ResolveImportFailed(String),
|
|
||||||
}
|
|
||||||
|
|
57
espanso-config/src/matches/group/path.rs
Normal file
57
espanso-config/src/matches/group/path.rs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use anyhow::Result;
|
||||||
|
use log::error;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
// TODO: test
|
||||||
|
pub fn resolve_imports(group_path: &Path, imports: &[String]) -> Result<Vec<String>> {
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
|
||||||
|
// Get the containing directory
|
||||||
|
let current_dir = if group_path.is_file() {
|
||||||
|
if let Some(parent) = group_path.parent() {
|
||||||
|
parent
|
||||||
|
} else {
|
||||||
|
return Err(
|
||||||
|
ResolveImportError::Failed(format!(
|
||||||
|
"unable to resolve imports for match group starting from current path: {:?}",
|
||||||
|
group_path
|
||||||
|
))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
group_path
|
||||||
|
};
|
||||||
|
|
||||||
|
for import in imports.iter() {
|
||||||
|
let import_path = PathBuf::from(import);
|
||||||
|
|
||||||
|
// Absolute or relative import
|
||||||
|
let full_path = if import_path.is_relative() {
|
||||||
|
current_dir.join(import_path)
|
||||||
|
} else {
|
||||||
|
import_path
|
||||||
|
};
|
||||||
|
|
||||||
|
if full_path.exists() && full_path.is_file() {
|
||||||
|
paths.push(full_path)
|
||||||
|
} else {
|
||||||
|
// Best effort imports
|
||||||
|
error!("unable to resolve import at path: {:?}", full_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let string_paths = paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| path.to_string_lossy().to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(string_paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ResolveImportError {
|
||||||
|
#[error("resolve import failed: `{0}`")]
|
||||||
|
Failed(String),
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ use crate::counter::{next_id, StructId};
|
||||||
mod group;
|
mod group;
|
||||||
mod store;
|
mod store;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Match {
|
pub struct Match {
|
||||||
cause: MatchCause,
|
cause: MatchCause,
|
||||||
effect: MatchEffect,
|
effect: MatchEffect,
|
||||||
|
@ -28,6 +28,12 @@ impl Default for Match {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Match {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.cause == other.cause && self.effect == other.effect && self.label == other.label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Causes
|
// Causes
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
@ -84,7 +90,7 @@ impl Default for TextEffect {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Variable {
|
pub struct Variable {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub var_type: String,
|
pub var_type: String,
|
||||||
|
@ -104,3 +110,9 @@ impl Default for Variable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Variable {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.name == other.name && self.var_type == other.var_type && self.params == other.params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,8 +4,11 @@ use std::{
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::MatchStore;
|
use super::{MatchSet, MatchStore};
|
||||||
use crate::{counter::StructId, matches::{group::MatchGroup, Match, Variable}};
|
use crate::{
|
||||||
|
counter::StructId,
|
||||||
|
matches::{group::MatchGroup, Match, Variable},
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: implement store according to notes
|
// TODO: implement store according to notes
|
||||||
pub(crate) struct DefaultMatchStore {
|
pub(crate) struct DefaultMatchStore {
|
||||||
|
@ -32,11 +35,27 @@ impl MatchStore for DefaultMatchStore {
|
||||||
|
|
||||||
// TODO: test
|
// TODO: test
|
||||||
// TODO: test for cyclical imports
|
// TODO: test for cyclical imports
|
||||||
fn query_set(&self, paths: &[String]) -> super::MatchSet {
|
fn query_set(&self, paths: &[String]) -> MatchSet {
|
||||||
let mut matches: Vec<&Match> = Vec::new();
|
let mut matches: Vec<&Match> = Vec::new();
|
||||||
let mut global_vars: Vec<&Variable> = Vec::new();
|
let mut global_vars: Vec<&Variable> = Vec::new();
|
||||||
|
let mut visited_paths = HashSet::new();
|
||||||
|
let mut visited_matches = HashSet::new();
|
||||||
|
let mut visited_global_vars = HashSet::new();
|
||||||
|
|
||||||
todo!()
|
query_matches_for_paths(
|
||||||
|
&self.groups,
|
||||||
|
&mut visited_paths,
|
||||||
|
&mut visited_matches,
|
||||||
|
&mut visited_global_vars,
|
||||||
|
&mut matches,
|
||||||
|
&mut global_vars,
|
||||||
|
paths,
|
||||||
|
);
|
||||||
|
|
||||||
|
MatchSet {
|
||||||
|
matches,
|
||||||
|
global_vars,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,9 +65,9 @@ fn load_match_groups_recursively(groups: &mut HashMap<String, MatchGroup>, paths
|
||||||
let group_path = PathBuf::from(path);
|
let group_path = PathBuf::from(path);
|
||||||
match MatchGroup::load(&group_path) {
|
match MatchGroup::load(&group_path) {
|
||||||
Ok(group) => {
|
Ok(group) => {
|
||||||
load_match_groups_recursively(groups, &group.resolved_imports);
|
load_match_groups_recursively(groups, &group.imports);
|
||||||
groups.insert(path.clone(), group);
|
groups.insert(path.clone(), group);
|
||||||
},
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
error!("unable to load match group: {:?}", error);
|
error!("unable to load match group: {:?}", error);
|
||||||
}
|
}
|
||||||
|
@ -57,8 +76,9 @@ fn load_match_groups_recursively(groups: &mut HashMap<String, MatchGroup>, paths
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: test
|
||||||
fn query_matches_for_paths<'a>(
|
fn query_matches_for_paths<'a>(
|
||||||
groups: &'a mut HashMap<String, MatchGroup>,
|
groups: &'a HashMap<String, MatchGroup>,
|
||||||
visited_paths: &mut HashSet<String>,
|
visited_paths: &mut HashSet<String>,
|
||||||
visited_matches: &mut HashSet<StructId>,
|
visited_matches: &mut HashSet<StructId>,
|
||||||
visited_global_vars: &mut HashSet<StructId>,
|
visited_global_vars: &mut HashSet<StructId>,
|
||||||
|
@ -83,7 +103,15 @@ fn query_matches_for_paths<'a>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: here we should visit the imported paths recursively
|
query_matches_for_paths(
|
||||||
|
groups,
|
||||||
|
visited_paths,
|
||||||
|
visited_matches,
|
||||||
|
visited_global_vars,
|
||||||
|
matches,
|
||||||
|
global_vars,
|
||||||
|
&group.imports,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
visited_paths.insert(path.clone());
|
visited_paths.insert(path.clone());
|
||||||
|
|
|
@ -13,8 +13,21 @@ pub fn is_yaml_empty(yaml: &str) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
pub mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use std::{fs::create_dir_all, path::Path};
|
||||||
|
use tempdir::TempDir;
|
||||||
|
|
||||||
|
pub fn use_test_directory(callback: impl FnOnce(&Path, &Path, &Path)) {
|
||||||
|
let dir = TempDir::new("tempconfig").unwrap();
|
||||||
|
let match_dir = dir.path().join("match");
|
||||||
|
create_dir_all(&match_dir).unwrap();
|
||||||
|
|
||||||
|
let config_dir = dir.path().join("config");
|
||||||
|
create_dir_all(&config_dir).unwrap();
|
||||||
|
|
||||||
|
callback(&dir.path(), &match_dir, &config_dir);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn is_yaml_empty_document_empty() {
|
fn is_yaml_empty_document_empty() {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user