use crate::selection::{RowSelection, SelectionCtx};
use anyhow::Result;
use rusqlite::Connection;
pub struct Pending<T> {
pub items: Vec<T>,
pub eligible: usize,
}
pub enum Work<T> {
Nothing(String),
Some(Pending<T>),
}
#[derive(Clone, Copy)]
pub struct Words {
pub verb: &'static str,
pub gerund: &'static str,
pub nothing_pending: Option<&'static str>,
}
impl Words {
pub const fn new(verb: &'static str, gerund: &'static str) -> Self {
Words {
verb,
gerund,
nothing_pending: None,
}
}
pub const fn saying(mut self, nothing_pending: &'static str) -> Self {
self.nothing_pending = Some(nothing_pending);
self
}
}
pub fn narrow<T>(
pending: Vec<T>,
hash_of: impl Fn(&T) -> &str,
selection: &RowSelection,
conn: &Connection,
ctx: &SelectionCtx,
words: Words,
silent: bool,
) -> Result<Work<T>> {
if pending.is_empty() {
return Ok(Work::Nothing(match words.nothing_pending {
Some(m) => m.to_string(),
None => format!(
"Nothing to {}: everything eligible is already done.",
words.verb
),
}));
}
let eligible = pending.len();
let items = if selection.is_empty() {
pending
} else {
let resolved = selection.resolve(conn, ctx)?;
match resolved.hashes {
None => pending,
Some(h) => pending
.into_iter()
.filter(|item| h.contains(hash_of(item)))
.collect(),
}
};
if !selection.is_empty() && !silent {
eprintln!(
"{} {} of {} pending item(s) ({})",
words.gerund,
items.len(),
eligible,
selection.describe()
);
}
if items.is_empty() {
return Ok(Work::Nothing(format!(
"Nothing to {}: the selection matched nothing pending.",
words.verb
)));
}
Ok(Work::Some(Pending { items, eligible }))
}
pub fn with_work<T, R>(
work: Work<T>,
silent: bool,
f: impl FnOnce(Pending<T>) -> Result<R>,
) -> Result<Option<R>> {
match work {
Work::Nothing(msg) => {
if !silent {
eprintln!("{msg}");
}
Ok(None)
}
Work::Some(pending) => f(pending).map(Some),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
const W: Words = Words::new("embed", "Embedding");
fn conn() -> Connection {
Connection::open_in_memory().unwrap()
}
fn db() -> Connection {
let c = Connection::open_in_memory().unwrap();
c.execute_batch(
"CREATE TABLE file_hashes (
path TEXT PRIMARY KEY, hash TEXT NOT NULL, size_bytes INTEGER,
created_at TEXT, modified_at TEXT, ext TEXT, mime TEXT, phash INTEGER,
exif_date TEXT, gps_lat REAL, gps_lon REAL, width INTEGER, height INTEGER);
INSERT INTO file_hashes (path, hash, ext, mime) VALUES
('/lib/a.jpg','h_jpg','jpg','image/jpeg'),
('/lib/b.mov','h_mov','mov','video/quicktime');",
)
.unwrap();
c
}
fn hash(s: &String) -> &str {
s.as_str()
}
#[test]
fn an_empty_pending_set_is_nothing_to_do() {
let c = conn();
let w = narrow(
Vec::<String>::new(),
hash,
&RowSelection::default(),
&c,
&SelectionCtx::default(),
W,
true,
)
.unwrap();
match w {
Work::Nothing(m) => {
assert_eq!(m, "Nothing to embed: everything eligible is already done.")
}
Work::Some(_) => panic!("an empty pending set must not be work"),
}
}
#[test]
fn no_selection_leaves_the_pending_set_untouched() {
let c = conn();
let w = narrow(
vec!["a".to_string(), "b".to_string()],
hash,
&RowSelection::default(),
&c,
&SelectionCtx::default(),
W,
true,
)
.unwrap();
match w {
Work::Some(p) => {
assert_eq!(p.items.len(), 2);
assert_eq!(p.eligible, 2, "eligible is the count before narrowing");
}
Work::Nothing(m) => panic!("unfiltered work was dropped: {m}"),
}
}
#[test]
fn a_caller_may_supply_its_own_empty_wording() {
let c = conn();
let w = narrow(
Vec::<String>::new(),
hash,
&RowSelection::default(),
&c,
&SelectionCtx::default(),
W.saying("All hashes already processed."),
true,
)
.unwrap();
match w {
Work::Nothing(m) => assert_eq!(m, "All hashes already processed."),
Work::Some(_) => panic!("an empty pending set must not be work"),
}
}
#[test]
fn a_selection_that_matches_nothing_is_nothing_to_do() {
let c = db();
let mut s = RowSelection::default();
s.exts = vec!["png".to_string()]; let w = narrow(
vec!["h_jpg".to_string(), "h_mov".to_string()],
hash,
&s,
&c,
&SelectionCtx::default(),
W,
true,
)
.unwrap();
match w {
Work::Nothing(m) => {
assert_eq!(
m,
"Nothing to embed: the selection matched nothing pending."
)
}
Work::Some(p) => panic!("{} item(s) survived a filter matching none", p.items.len()),
}
}
#[test]
fn a_selection_keeps_only_what_it_matched() {
let c = db();
let mut s = RowSelection::default();
s.exts = vec!["jpg".to_string()];
let w = narrow(
vec!["h_jpg".to_string(), "h_mov".to_string()],
hash,
&s,
&c,
&SelectionCtx::default(),
W,
true,
)
.unwrap();
match w {
Work::Some(p) => {
assert_eq!(p.items, vec!["h_jpg".to_string()]);
assert_eq!(p.eligible, 2, "the denominator is the pre-filter count");
}
Work::Nothing(m) => panic!("a matching filter dropped everything: {m}"),
}
}
#[test]
fn the_closure_never_runs_when_there_is_nothing_to_do() {
let ran = Cell::new(false);
let out = with_work(Work::<String>::Nothing("nothing".into()), true, |_| {
ran.set(true);
Ok(())
})
.unwrap();
assert!(
!ran.get(),
"no work must mean no closure, and so no model load"
);
assert!(out.is_none());
}
#[test]
fn the_closure_runs_and_returns_its_value_when_there_is_work() {
let ran = Cell::new(false);
let out = with_work(
Work::Some(Pending {
items: vec!["a".to_string()],
eligible: 1,
}),
true,
|p| {
ran.set(true);
Ok(p.items.len())
},
)
.unwrap();
assert!(ran.get());
assert_eq!(out, Some(1));
}
#[test]
fn an_error_from_the_closure_is_not_swallowed() {
let out = with_work(
Work::Some(Pending {
items: vec!["a".to_string()],
eligible: 1,
}),
true,
|_| -> Result<()> { anyhow::bail!("boom") },
);
assert!(
out.is_err(),
"the closure's failure is the caller's failure"
);
}
}