Refactor config structure and improve importer logic

This commit is contained in:
Federico Terzi 2021-03-05 21:31:54 +01:00
parent 2cb8da91a5
commit 3974d90bc9
12 changed files with 399 additions and 248 deletions

View File

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

View File

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

View File

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

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

View File

@ -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,16 +136,11 @@ 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 super::*; use serde_yaml::{Mapping, Value};
use super::*;
use crate::matches::Match; use crate::matches::Match;
fn create_match(yaml: &str) -> Result<Match> { fn create_match(yaml: &str) -> Result<Match> {

View 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()
}

View File

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

View 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),
}

View File

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

View File

@ -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());

View File

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