use anyhow::Result;
use std::collections::HashSet;
use std::path::Path;
use parking_lot::Mutex;
use crate::processors::flush_words;
use crate::graph::Product;
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}");
}
}