use std::error::Error;
use std::fmt::{Display, Formatter};
use indexmap::IndexSet;
use super::{PatternUtils, PatternUtilsError, StringPattern, Validate, ValidateError};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PatternSpecError {
Validation(ValidateError),
Pattern(PatternUtilsError),
}
impl PatternSpecError {
#[must_use]
pub const fn class_name(&self) -> &'static str {
match self {
Self::Validation(error) => error.class_name(),
Self::Pattern(error) => error.class_name(),
}
}
#[must_use]
pub fn get_message(&self) -> Option<&str> {
match self {
Self::Validation(error) => error.get_message(),
Self::Pattern(error) => error.get_message(),
}
}
}
impl Display for PatternSpecError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Validation(error) => Display::fmt(error, formatter),
Self::Pattern(error) => Display::fmt(error, formatter),
}
}
}
impl Error for PatternSpecError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Validation(error) => Some(error),
Self::Pattern(error) => Some(error),
}
}
}
impl From<ValidateError> for PatternSpecError {
fn from(error: ValidateError) -> Self {
Self::Validation(error)
}
}
impl From<PatternUtilsError> for PatternSpecError {
fn from(error: PatternUtilsError) -> Self {
Self::Pattern(error)
}
}
#[derive(Clone, Debug, Default)]
pub struct PatternSpec {
pattern_strs: IndexSet<Option<String>>,
patterns: Vec<StringPattern>,
}
impl PatternSpec {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.patterns.is_empty()
}
#[must_use]
pub fn get_patterns(&self) -> &IndexSet<Option<String>> {
&self.pattern_strs
}
pub fn set_patterns(
&mut self,
new_patterns: Option<&[Option<&str>]>,
) -> Result<(), PatternSpecError> {
self.pattern_strs.clear();
self.patterns.clear();
let Some(new_patterns) = new_patterns else {
return Ok(());
};
for pattern in new_patterns {
self.pattern_strs.insert(pattern.map(ToOwned::to_owned));
}
for pattern in &self.pattern_strs {
self.patterns
.push(PatternUtils::str_pattern_to_pattern(pattern.as_deref())?);
}
Ok(())
}
pub fn add_pattern(&mut self, pattern: Option<&str>) -> Result<(), PatternSpecError> {
Validate::not_empty_str(pattern, Some("Pattern cannot be null or empty"))?;
let pattern = pattern.expect("validated pattern");
self.pattern_strs.insert(Some(pattern.to_owned()));
self.patterns
.push(PatternUtils::str_pattern_to_pattern(Some(pattern))?);
Ok(())
}
pub fn clear_patterns(&mut self) {
self.pattern_strs.clear();
self.patterns.clear();
}
pub fn matches(&self, template_name: Option<&str>) -> Result<bool, PatternSpecError> {
for pattern in &self.patterns {
if pattern.matches(template_name)? {
return Ok(true);
}
}
Ok(false)
}
#[cfg(test)]
fn compiled_pattern_count(&self) -> usize {
self.patterns.len()
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use std::fmt::Write;
use super::PatternSpec;
struct FailingWriter;
impl Write for FailingWriter {
fn write_str(&mut self, _value: &str) -> std::fmt::Result {
Err(std::fmt::Error)
}
}
#[test]
fn new_get_clear_and_null_set_preserve_empty_contracts() {
let mut spec = PatternSpec::new();
assert!(spec.is_empty());
assert!(spec.get_patterns().is_empty());
assert_eq!(spec.matches(None), Ok(false));
assert_eq!(spec.set_patterns(None), Ok(()));
spec.clear_patterns();
assert!(spec.is_empty());
}
#[test]
fn sets_deduplicated_ordered_patterns_and_matches_in_full() {
let mut spec = PatternSpec::new();
spec.set_patterns(Some(&[Some("*.html"), Some("admin/*"), Some("*.html")]))
.expect("patterns");
assert_eq!(
spec.get_patterns().iter().collect::<Vec<_>>(),
vec![&Some("*.html".to_owned()), &Some("admin/*".to_owned())]
);
assert_eq!(spec.compiled_pattern_count(), 2);
assert_eq!(spec.matches(Some("index.html")), Ok(true));
assert_eq!(spec.matches(Some("admin/users")), Ok(true));
assert_eq!(spec.matches(Some("index.htm")), Ok(false));
}
#[test]
fn repeated_add_keeps_one_public_string_and_multiple_pattern_identities() {
let mut spec = PatternSpec::new();
spec.add_pattern(Some("*.html")).expect("first");
spec.add_pattern(Some("*.html")).expect("second");
assert_eq!(spec.get_patterns().len(), 1);
assert_eq!(spec.compiled_pattern_count(), 2);
assert_eq!(spec.matches(Some("view.html")), Ok(true));
spec.clear_patterns();
assert_eq!(spec.compiled_pattern_count(), 0);
}
#[test]
fn add_validates_before_mutation_and_preserves_failed_compile_string() {
let mut spec = PatternSpec::new();
for invalid in [None, Some(""), Some("\u{2008}")] {
let error = spec.add_pattern(invalid).expect_err("validation");
assert_eq!(error.class_name(), "java.lang.IllegalArgumentException");
assert_eq!(error.get_message(), Some("Pattern cannot be null or empty"));
assert_eq!(error.to_string(), "Pattern cannot be null or empty");
assert!(error.source().is_some());
assert!(write!(&mut FailingWriter, "{error}").is_err());
}
assert!(spec.get_patterns().is_empty());
let syntax = spec.add_pattern(Some("{")).expect_err("syntax");
assert_eq!(
syntax.class_name(),
"java.util.regex.PatternSyntaxException"
);
assert_eq!(spec.get_patterns().len(), 1);
assert!(spec.is_empty());
assert!(!syntax.to_string().is_empty());
assert!(syntax.source().is_some());
assert!(write!(&mut FailingWriter, "{syntax}").is_err());
}
#[test]
fn set_failure_keeps_all_strings_and_compiled_prefix_without_rollback() {
let mut spec = PatternSpec::new();
let error = spec
.set_patterns(Some(&[Some("*.html"), Some("{"), Some("*.txt")]))
.expect_err("syntax");
assert_eq!(error.class_name(), "java.util.regex.PatternSyntaxException");
assert_eq!(spec.get_patterns().len(), 3);
assert_eq!(spec.compiled_pattern_count(), 1);
assert_eq!(spec.matches(Some("view.html")), Ok(true));
assert_eq!(spec.matches(Some("view.txt")), Ok(false));
let null = spec
.set_patterns(Some(&[Some("*.html"), None, Some("*.txt")]))
.expect_err("null");
assert_eq!(null.class_name(), "java.lang.NullPointerException");
assert_eq!(spec.get_patterns().len(), 3);
assert_eq!(spec.compiled_pattern_count(), 1);
}
#[test]
fn null_template_errors_only_when_a_compiled_pattern_is_visited() {
let mut spec = PatternSpec::new();
assert_eq!(spec.matches(None), Ok(false));
spec.add_pattern(Some("*")).expect("pattern");
let error = spec.matches(None).expect_err("null");
assert_eq!(error.class_name(), "java.lang.NullPointerException");
assert_eq!(error.get_message(), None);
assert!(error.source().is_some());
}
}