use anyhow::Result;
use parking_lot::Mutex;
use std::collections::HashSet;
use std::path::Path;
use crate::graph::Product;
use crate::processors::flush_words;
pub struct WordManager {
custom_words: Mutex<HashSet<String>>,
words_to_add: Mutex<HashSet<String>>,
words_file: String,
header_line: Option<&'static str>,
}
impl WordManager {
pub fn new(
custom_words: HashSet<String>,
words_file: String,
header_line: Option<&'static str>,
) -> Self {
Self {
custom_words: Mutex::new(custom_words),
words_to_add: Mutex::new(HashSet::new()),
words_file,
header_line,
}
}
pub fn is_known(&self, word: &str) -> bool {
self.custom_words.lock().contains(word)
}
pub fn handle_misspelled(
&self,
misspelled: &[impl AsRef<str>],
file: &Path,
auto_add_words: bool,
) -> Result<()> {
if misspelled.is_empty() {
return Ok(());
}
if auto_add_words {
let mut words_to_add = self.words_to_add.lock();
for word in misspelled {
words_to_add.insert(word.as_ref().to_lowercase());
}
drop(words_to_add);
Ok(())
} else {
let words: Vec<&str> = misspelled.iter().map(std::convert::AsRef::as_ref).collect();
anyhow::bail!(
"Misspelled words in {}:\n{}",
file.display(),
words.join("\n"),
)
}
}
pub fn flush(&self) -> Result<()> {
let mut words_to_add = self.words_to_add.lock();
if words_to_add.is_empty() {
return Ok(());
}
let mut custom_words = self.custom_words.lock();
let words_path = Path::new(&self.words_file);
flush_words(&custom_words, &words_to_add, words_path, self.header_line)?;
custom_words.extend(words_to_add.drain());
Ok(())
}
pub fn execute_with_flush(
&self,
product: &Product,
auto_add_words: bool,
check_fn: impl FnOnce(&Path) -> Result<()>,
processor_name: &str,
) -> Result<()> {
let result = check_fn(product.primary_input());
if auto_add_words && let Err(e) = self.flush() {
return result.and(Err(
e.context(format!("Failed to flush {processor_name} words file"))
));
}
result
}
pub fn execute_batch_with_flush(
&self,
products: &[&Product],
auto_add_words: bool,
check_fn: impl Fn(&Path) -> Result<()>,
processor_name: &str,
) -> Vec<Result<()>> {
let mut results: Vec<Result<()>> = products
.iter()
.map(|p| check_fn(p.primary_input()))
.collect();
if auto_add_words && let Err(e) = self.flush() {
let msg = format!("Failed to flush {processor_name} words file: {e:#}");
for r in &mut results {
if r.is_ok() {
*r = Err(anyhow::anyhow!("{msg}"));
}
}
}
results
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flush_does_not_reappend_words() {
let tmp = tempfile::TempDir::new().unwrap();
let words_file = tmp.path().join("words.txt");
let mgr = WordManager::new(HashSet::new(), words_file.display().to_string(), None);
mgr.handle_misspelled(&["Frobnicate"], Path::new("a.md"), true)
.unwrap();
mgr.flush().unwrap();
assert!(mgr.is_known("frobnicate"), "flushed word must become known");
mgr.flush().unwrap();
mgr.handle_misspelled(&["quuxify"], Path::new("b.md"), true)
.unwrap();
mgr.flush().unwrap();
let content = std::fs::read_to_string(&words_file).unwrap();
let frob_count = content.lines().filter(|l| *l == "frobnicate").count();
let quux_count = content.lines().filter(|l| *l == "quuxify").count();
assert_eq!(
frob_count, 1,
"word must appear exactly once, got:\n{content}"
);
assert_eq!(
quux_count, 1,
"word must appear exactly once, got:\n{content}"
);
}
}