use std::ops::Range;
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, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum MatchOverlap {
#[default]
LeftmostLongest,
All,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MatcherOptions {
pub case_insensitive: bool,
pub min_pattern_length: usize,
#[serde(default)]
pub overlap: MatchOverlap,
#[serde(default = "default_word_boundaries")]
pub word_boundaries: bool,
#[serde(default)]
pub allow_blank_patterns: bool,
}
fn default_word_boundaries() -> bool {
true
}
impl Default for MatcherOptions {
fn default() -> Self {
Self {
case_insensitive: true,
min_pattern_length: DEFAULT_MIN_PATTERN_LENGTH,
overlap: MatchOverlap::LeftmostLongest,
word_boundaries: default_word_boundaries(),
allow_blank_patterns: false,
}
}
}
impl MatcherOptions {
#[must_use]
pub const fn with_case_insensitive(mut self, case_insensitive: bool) -> Self {
self.case_insensitive = case_insensitive;
self
}
#[must_use]
pub const fn with_min_pattern_length(mut self, min_pattern_length: usize) -> Self {
self.min_pattern_length = min_pattern_length;
self
}
#[must_use]
pub const fn with_overlap(mut self, overlap: MatchOverlap) -> Self {
self.overlap = overlap;
self
}
#[must_use]
pub const fn with_word_boundaries(mut self, word_boundaries: bool) -> Self {
self.word_boundaries = word_boundaries;
self
}
#[must_use]
pub const fn with_allow_blank_patterns(mut self, allow_blank_patterns: bool) -> Self {
self.allow_blank_patterns = allow_blank_patterns;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub struct PositionedMatch {
pub start: usize,
pub end: usize,
pub pattern_index: usize,
}
impl PositionedMatch {
#[must_use]
pub const fn new(start: usize, end: usize, pattern_index: usize) -> Self {
Self {
start,
end,
pattern_index,
}
}
#[must_use]
pub const fn range(&self) -> Range<usize> {
self.start..self.end
}
}
impl From<aho_corasick::Match> for PositionedMatch {
#[inline]
fn from(mat: aho_corasick::Match) -> Self {
Self {
start: mat.start(),
end: mat.end(),
pattern_index: mat.pattern().as_usize(),
}
}
}
#[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() && (pattern.is_empty() || !self.options.allow_blank_patterns) {
let reason = if self.options.allow_blank_patterns {
"pattern is empty"
} else {
"pattern is blank"
};
return Err(TerraphimAutomataError::InvalidPattern {
pattern: pattern.clone(),
reason: reason.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 match_kind = match self.options.overlap {
MatchOverlap::LeftmostLongest => MatchKind::LeftmostLongest,
MatchOverlap::All => MatchKind::Standard,
};
let ac = AhoCorasick::builder()
.match_kind(match_kind)
.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 pattern(&self, pattern_index: usize) -> Option<&str> {
self.inner
.patterns
.get(pattern_index)
.map(|(pattern, _)| pattern.as_str())
}
pub fn term(&self, pattern_index: usize) -> Option<&NormalizedTerm> {
self.inner.patterns.get(pattern_index).map(|(_, term)| term)
}
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> {
let options = &self.inner.options;
let raw = match options.overlap {
MatchOverlap::LeftmostLongest if options.word_boundaries => RawMatches::Leftmost(
self.inner
.ac
.find_iter(text)
.filter(move |mat| {
crate::matcher::is_word_boundary_match(text, mat.start(), mat.end())
}),
),
MatchOverlap::LeftmostLongest => {
RawMatches::LeftmostAnywhere(self.inner.ac.find_iter(text))
}
MatchOverlap::All => RawMatches::Collected(self.all_matches(text).into_iter()),
};
raw.map(move |mat| self.matched_for(mat, false))
}
pub fn find_positions<'a>(
&'a self,
text: &'a str,
) -> impl Iterator<Item = PositionedMatch> + use<'a> {
let ac = &self.inner.ac;
let word_boundaries = self.inner.options.word_boundaries;
let raw = match self.inner.options.overlap {
MatchOverlap::LeftmostLongest => RawPositions::Leftmost(ac.find_iter(text)),
MatchOverlap::All => RawPositions::Overlapping(ac.find_overlapping_iter(text)),
};
raw.filter(move |mat| {
!word_boundaries || crate::matcher::is_word_boundary_match(text, mat.start(), mat.end())
})
.map(PositionedMatch::from)
}
pub fn push_positions(&self, text: &str, buffer: &mut Vec<PositionedMatch>) {
let first_new = buffer.len();
buffer.extend(self.find_positions(text));
if self.inner.options.overlap == MatchOverlap::All {
buffer[first_new..].sort_unstable();
}
}
fn push_matches_impl(
&self,
text: &str,
return_positions: bool,
out: &mut Vec<Matched>,
) -> Result<()> {
let options = &self.inner.options;
match options.overlap {
MatchOverlap::LeftmostLongest if options.word_boundaries => {
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));
}
}
MatchOverlap::LeftmostLongest => {
out.extend(
self.inner
.ac
.find_iter(text)
.map(|mat| self.matched_for(mat, return_positions)),
);
}
MatchOverlap::All => {
out.extend(
self.all_matches(text)
.into_iter()
.map(|mat| self.matched_for(mat, return_positions)),
);
}
}
Ok(())
}
fn all_matches(&self, text: &str) -> Vec<aho_corasick::Match> {
let word_boundaries = self.inner.options.word_boundaries;
let mut matches: Vec<aho_corasick::Match> = self
.inner
.ac
.find_overlapping_iter(text)
.filter(|mat| {
!word_boundaries
|| crate::matcher::is_word_boundary_match(text, mat.start(), mat.end())
})
.collect();
matches.sort_unstable_by_key(|mat| (mat.start(), mat.end(), mat.pattern()));
matches
}
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
},
}
}
}
enum RawPositions<'a> {
Leftmost(aho_corasick::FindIter<'a, 'a>),
Overlapping(aho_corasick::FindOverlappingIter<'a, 'a>),
}
impl Iterator for RawPositions<'_> {
type Item = aho_corasick::Match;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Leftmost(iter) => iter.next(),
Self::Overlapping(iter) => iter.next(),
}
}
}
enum RawMatches<L, A> {
Leftmost(L),
LeftmostAnywhere(A),
Collected(std::vec::IntoIter<aho_corasick::Match>),
}
impl<L, A> Iterator for RawMatches<L, A>
where
L: Iterator<Item = aho_corasick::Match>,
A: Iterator<Item = aho_corasick::Match>,
{
type Item = aho_corasick::Match;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Leftmost(iter) => iter.next(),
Self::LeftmostAnywhere(iter) => iter.next(),
Self::Collected(iter) => iter.next(),
}
}
}