use std::sync::Arc;
use aho_corasick::{AhoCorasick, MatchKind};
use serde::{Deserialize, Serialize};
use terraphim_types::NormalizedTerm;
use crate::matcher::Matched;
use crate::{Result, TerraphimAutomataError};
pub const DEFAULT_MIN_PATTERN_LENGTH: usize = 2;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MatcherOptions {
pub case_insensitive: bool,
pub min_pattern_length: usize,
}
impl Default for MatcherOptions {
fn default() -> Self {
Self {
case_insensitive: true,
min_pattern_length: DEFAULT_MIN_PATTERN_LENGTH,
}
}
}
#[derive(Debug, Clone)]
pub struct MatcherBuilder {
options: MatcherOptions,
patterns: Vec<(String, NormalizedTerm)>,
}
impl MatcherBuilder {
pub fn new(options: MatcherOptions) -> Self {
Self {
options,
patterns: Vec::new(),
}
}
pub fn insert(&mut self, pattern: String, term: NormalizedTerm) -> Result<&mut Self> {
let trimmed = pattern.trim();
let trimmed_len = pattern.len();
if trimmed.is_empty() {
return Err(TerraphimAutomataError::InvalidPattern {
pattern: pattern.clone(),
reason: "pattern is blank".to_string(),
});
}
if trimmed_len < self.options.min_pattern_length {
return Err(TerraphimAutomataError::InvalidPattern {
pattern: pattern.clone(),
reason: format!(
"pattern length {} is below the configured minimum {}",
trimmed_len, self.options.min_pattern_length
),
});
}
if self
.patterns
.iter()
.any(|(existing, _)| existing == &pattern)
{
return Err(TerraphimAutomataError::DuplicatePattern(pattern));
}
self.patterns.push((pattern, term));
Ok(self)
}
pub fn with_pattern(mut self, pattern: String, term: NormalizedTerm) -> Result<Self> {
self.insert(pattern, term)?;
Ok(self)
}
pub fn len(&self) -> usize {
self.patterns.len()
}
pub fn is_empty(&self) -> bool {
self.patterns.is_empty()
}
pub fn build(self) -> Result<CompiledMatcher> {
let ac = AhoCorasick::builder()
.match_kind(MatchKind::LeftmostLongest)
.ascii_case_insensitive(self.options.case_insensitive)
.build(self.patterns.iter().map(|(p, _)| p.as_str()))?;
Ok(CompiledMatcher {
inner: Arc::new(CompiledMatcherInner {
ac,
patterns: self.patterns,
options: self.options,
}),
})
}
}
#[derive(Debug)]
struct CompiledMatcherInner {
ac: AhoCorasick,
patterns: Vec<(String, NormalizedTerm)>,
options: MatcherOptions,
}
#[derive(Debug, Clone)]
pub struct CompiledMatcher {
inner: Arc<CompiledMatcherInner>,
}
const _: () = {
const fn assert_send_sync<T: Send + Sync>() {}
let _ = assert_send_sync::<CompiledMatcher>;
};
impl CompiledMatcher {
pub fn from_thesaurus(
thesaurus: &terraphim_types::Thesaurus,
options: MatcherOptions,
) -> Result<Self> {
let mut builder = MatcherBuilder::new(options);
for (key, term) in thesaurus.into_iter() {
builder.insert(key.to_string(), term.clone())?;
}
builder.build()
}
pub fn options(&self) -> &MatcherOptions {
&self.inner.options
}
pub fn patterns(&self) -> &[(String, NormalizedTerm)] {
&self.inner.patterns
}
pub fn len(&self) -> usize {
self.inner.patterns.len()
}
pub fn is_empty(&self) -> bool {
self.inner.patterns.is_empty()
}
pub fn find_matches(&self, text: &str, return_positions: bool) -> Result<Vec<Matched>> {
let mut out = Vec::new();
self.push_matches_impl(text, return_positions, &mut out)?;
Ok(out)
}
pub fn push_matches(&self, text: &str, buffer: &mut Vec<Matched>) -> Result<()> {
self.push_matches_impl(text, false, buffer)
}
pub fn find_iter<'text>(
&'text self,
text: &'text str,
) -> impl Iterator<Item = Matched> + use<'text> {
self.inner
.ac
.find_iter(text)
.filter(move |mat| crate::matcher::is_word_boundary_match(text, mat.start(), mat.end()))
.map(move |mat| self.matched_for(mat, false))
}
fn push_matches_impl(
&self,
text: &str,
return_positions: bool,
out: &mut Vec<Matched>,
) -> Result<()> {
for mat in self.inner.ac.find_iter(text) {
if !crate::matcher::is_word_boundary_match(text, mat.start(), mat.end()) {
continue;
}
out.push(self.matched_for(mat, return_positions));
}
Ok(())
}
fn matched_for(&self, mat: aho_corasick::Match, return_positions: bool) -> Matched {
let (term, normalized_term) = &self.inner.patterns[mat.pattern().as_usize()];
Matched {
term: term.clone(),
normalized_term: normalized_term.clone(),
pos: if return_positions {
Some((mat.start(), mat.end()))
} else {
None
},
}
}
}