diff --git a/src/parser.rs b/src/parser.rs index eb88c24..33ed946 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,13 +1,64 @@ use crate::ruleset::{Glob, Regex, Rule, Ruleset}; -use serde::{Deserialize, Serialize}; +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize}; use std::fs; use std::path::{Path, PathBuf}; +struct OptionalStringSequenceVisitor; + +impl<'de> serde::de::Visitor<'de> for OptionalStringSequenceVisitor { + type Value = Option>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("string with whitespace separated values or sequence of strings") + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error, + { + let res: Vec<_> = s.split_whitespace().map(|s| s.to_string()).collect(); + if res.is_empty() { + Err(E::custom("empty string not allowed")) + } else { + Ok(Some(res)) + } + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut res = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + + while let Some(value) = seq.next_element::()? { + res.push(value); + } + + if res.is_empty() { + Err(A::Error::custom("empty sequence not allowed")) + } else { + Ok(Some(res)) + } + } +} + +fn deserialize_optional_string_sequence<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(OptionalStringSequenceVisitor) +} + #[derive(Deserialize, Serialize, PartialEq, Debug)] struct ParsedRule { pub title: String, - pub files: Option, - pub nofiles: Option, + #[serde(default, deserialize_with = "deserialize_optional_string_sequence")] + pub files: Option>, + #[serde(default, deserialize_with = "deserialize_optional_string_sequence")] + pub nofiles: Option>, #[serde(rename(serialize = "match", deserialize = "match"))] pub pattern: Option, } @@ -63,8 +114,18 @@ impl Config { .into_iter() .map(|parsed_rule| Rule { title: parsed_rule.title, - globs: parsed_rule.files.map(|g| vec![Glob::new(&g).unwrap()]), - antiglobs: parsed_rule.nofiles.map(|g| vec![Glob::new(&g).unwrap()]), + globs: parsed_rule.files.map(|patterns| { + patterns + .iter() + .map(|pattern| Glob::new(&pattern).unwrap()) + .collect() + }), + antiglobs: parsed_rule.nofiles.map(|patterns| { + patterns + .iter() + .map(|pattern| Glob::new(&pattern).unwrap()) + .collect() + }), regex: parsed_rule.pattern.map(|p| Regex::new(&p).unwrap()), }) .collect::>(), diff --git a/tests/suite.rs b/tests/suite.rs index fd140a1..d886e7c 100644 --- a/tests/suite.rs +++ b/tests/suite.rs @@ -115,7 +115,6 @@ fn glob_scope() { } #[test] -#[ignore] fn multiple_globs() { TestCase::new() .add_file("a.py", "") @@ -128,6 +127,23 @@ fn multiple_globs() { ", ) .assert_matches(vec!["a.py", "a.txt", "a.rs"]); + + TestCase::new() + .add_file("README", "") + .run_with_rule( + " + - title: nofiles should not match if any pattern matches + nofiles: 'README README.txt' + ", + ) + .assert_matches(vec![]) + .run_with_rule( + " + - title: nofiles should match if neither pattern matches + nofiles: 'README.txt README.md' + ", + ) + .assert_matches(vec![""]); } #[test] @@ -149,3 +165,25 @@ fn nofiles() { ) .assert_matches(vec![]); } + +#[test] +#[should_panic] +fn empty_globs_string() { + TestCase::new().run_with_rule( + " + - title: nofiles which matches + files: ' ' + ", + ); +} + +#[test] +#[should_panic] +fn empty_globs_seq() { + TestCase::new().run_with_rule( + " + - title: nofiles which matches + files: [] + ", + ); +}