#![allow(dead_code)]
use super::{ClipFlag, ClipRating, RatingDatabase, RatingEntry};
#[derive(Debug, Clone)]
pub struct BatchRatingCommand {
pub clip_ids: Vec<u64>,
pub rating: Option<ClipRating>,
pub flag: Option<ClipFlag>,
pub append_note: String,
}
impl BatchRatingCommand {
#[must_use]
pub fn set_rating(clip_ids: Vec<u64>, rating: ClipRating) -> Self {
Self {
clip_ids,
rating: Some(rating),
flag: None,
append_note: String::new(),
}
}
#[must_use]
pub fn set_flag(clip_ids: Vec<u64>, flag: ClipFlag) -> Self {
Self {
clip_ids,
rating: None,
flag: Some(flag),
append_note: String::new(),
}
}
#[must_use]
pub fn set_rating_and_flag(clip_ids: Vec<u64>, rating: ClipRating, flag: ClipFlag) -> Self {
Self {
clip_ids,
rating: Some(rating),
flag: Some(flag),
append_note: String::new(),
}
}
#[must_use]
pub fn with_note(mut self, note: impl Into<String>) -> Self {
self.append_note = note.into();
self
}
pub fn apply(&self, db: &mut RatingDatabase) -> usize {
let mut count = 0;
for &id in &self.clip_ids {
if let Some(rating) = self.rating {
db.set_rating(id, rating);
}
if let Some(flag) = self.flag {
db.set_flag(id, flag);
}
if !self.append_note.is_empty() {
let existing = db.get(id).map(|e| e.notes.clone()).unwrap_or_default();
let new_notes = if existing.is_empty() {
self.append_note.clone()
} else {
format!("{} {}", existing, self.append_note)
};
db.set_notes(id, new_notes);
}
count += 1;
}
count
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BatchResult {
pub processed: usize,
pub skipped: usize,
}
impl BatchResult {
#[must_use]
pub fn new(processed: usize, skipped: usize) -> Self {
Self { processed, skipped }
}
}
pub fn apply_idempotent(cmd: &BatchRatingCommand, db: &mut RatingDatabase) -> BatchResult {
let mut processed = 0;
let mut skipped = 0;
for &id in &cmd.clip_ids {
let already_rated = cmd.rating.map_or(false, |target_rating| {
db.get(id)
.map(|e| e.rating == target_rating)
.unwrap_or(false)
});
if already_rated && cmd.flag.is_none() && cmd.append_note.is_empty() {
skipped += 1;
continue;
}
if let Some(rating) = cmd.rating {
db.set_rating(id, rating);
}
if let Some(flag) = cmd.flag {
db.set_flag(id, flag);
}
if !cmd.append_note.is_empty() {
let existing = db.get(id).map(|e| e.notes.clone()).unwrap_or_default();
let new_notes = if existing.is_empty() {
cmd.append_note.clone()
} else {
format!("{} {}", existing, cmd.append_note)
};
db.set_notes(id, new_notes);
}
processed += 1;
}
BatchResult::new(processed, skipped)
}
#[must_use]
pub fn select_by_rating(db: &RatingDatabase, min: ClipRating) -> Vec<u64> {
db.all()
.into_iter()
.filter(|e| e.rating >= min)
.map(|e| e.clip_id)
.collect()
}
#[must_use]
pub fn select_by_flag(db: &RatingDatabase, flag: ClipFlag) -> Vec<u64> {
db.all()
.into_iter()
.filter(|e| e.flag == flag)
.map(|e| e.clip_id)
.collect()
}
pub fn reject_below(db: &mut RatingDatabase, threshold: ClipRating) -> Vec<u64> {
let to_reject: Vec<u64> = db
.all()
.into_iter()
.filter(|e| e.rating < threshold)
.map(|e| e.clip_id)
.collect();
for &id in &to_reject {
db.set_rating(id, ClipRating::Reject);
}
to_reject
}
#[must_use]
pub fn export_csv(db: &RatingDatabase) -> String {
let mut lines = vec!["clip_id,rating,flag,notes".to_string()];
let mut entries: Vec<&RatingEntry> = db.all();
entries.sort_by_key(|e| e.clip_id);
for e in entries {
lines.push(format!(
"{},{},{},{}",
e.clip_id,
e.rating.label(),
e.flag.name(),
e.notes
));
}
lines.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
fn populated_db() -> RatingDatabase {
let mut db = RatingDatabase::new();
db.set_rating(1, ClipRating::Excellent);
db.set_flag(1, ClipFlag::Green);
db.set_rating(2, ClipRating::Ok);
db.set_flag(2, ClipFlag::Yellow);
db.set_rating(3, ClipRating::Pickup);
db.set_flag(3, ClipFlag::Red);
db.set_rating(4, ClipRating::Unrated);
db
}
#[test]
fn test_batch_set_rating_apply() {
let mut db = populated_db();
let cmd = BatchRatingCommand::set_rating(vec![2, 3], ClipRating::Good);
let count = cmd.apply(&mut db);
assert_eq!(count, 2);
assert_eq!(
db.get(2).expect("get should succeed").rating,
ClipRating::Good
);
assert_eq!(
db.get(3).expect("get should succeed").rating,
ClipRating::Good
);
assert_eq!(
db.get(1).expect("get should succeed").rating,
ClipRating::Excellent
);
}
#[test]
fn test_batch_set_flag_apply() {
let mut db = populated_db();
let cmd = BatchRatingCommand::set_flag(vec![1, 2], ClipFlag::Blue);
cmd.apply(&mut db);
assert_eq!(db.get(1).expect("get should succeed").flag, ClipFlag::Blue);
assert_eq!(db.get(2).expect("get should succeed").flag, ClipFlag::Blue);
}
#[test]
fn test_batch_set_rating_and_flag() {
let mut db = populated_db();
let cmd = BatchRatingCommand::set_rating_and_flag(
vec![3, 4],
ClipRating::Excellent,
ClipFlag::Green,
);
let count = cmd.apply(&mut db);
assert_eq!(count, 2);
assert_eq!(
db.get(3).expect("get should succeed").rating,
ClipRating::Excellent
);
assert_eq!(db.get(4).expect("get should succeed").flag, ClipFlag::Green);
}
#[test]
fn test_batch_with_note() {
let mut db = RatingDatabase::new();
db.set_rating(10, ClipRating::Good);
let cmd = BatchRatingCommand::set_rating(vec![10], ClipRating::Good)
.with_note("approved by director");
cmd.apply(&mut db);
assert!(db
.get(10)
.expect("get should succeed")
.notes
.contains("approved by director"));
}
#[test]
fn test_batch_note_appended() {
let mut db = RatingDatabase::new();
db.set_rating(5, ClipRating::Ok);
db.set_notes(5, "first note");
let cmd = BatchRatingCommand::set_rating(vec![5], ClipRating::Ok).with_note("second note");
cmd.apply(&mut db);
let notes = &db.get(5).expect("get should succeed").notes;
assert!(notes.contains("first note"));
assert!(notes.contains("second note"));
}
#[test]
fn test_apply_idempotent_already_rated() {
let mut db = populated_db();
let cmd = BatchRatingCommand::set_rating(vec![1], ClipRating::Excellent);
let result = apply_idempotent(&cmd, &mut db);
assert_eq!(result.skipped, 1);
assert_eq!(result.processed, 0);
}
#[test]
fn test_apply_idempotent_changes_applied() {
let mut db = populated_db();
let cmd = BatchRatingCommand::set_rating(vec![2, 3], ClipRating::Excellent);
let result = apply_idempotent(&cmd, &mut db);
assert_eq!(result.processed, 2);
assert_eq!(result.skipped, 0);
}
#[test]
fn test_select_by_rating() {
let db = populated_db();
let ids = select_by_rating(&db, ClipRating::Ok);
assert!(ids.contains(&1));
assert!(ids.contains(&2));
assert!(!ids.contains(&3));
}
#[test]
fn test_select_by_flag() {
let db = populated_db();
let ids = select_by_flag(&db, ClipFlag::Green);
assert_eq!(ids, vec![1]);
}
#[test]
fn test_reject_below() {
let mut db = populated_db();
let rejected = reject_below(&mut db, ClipRating::Ok);
assert!(rejected.contains(&3));
assert!(rejected.contains(&4));
assert_eq!(
db.get(3).expect("get should succeed").rating,
ClipRating::Reject
);
assert_eq!(
db.get(4).expect("get should succeed").rating,
ClipRating::Reject
);
assert_eq!(
db.get(1).expect("get should succeed").rating,
ClipRating::Excellent
);
assert_eq!(
db.get(2).expect("get should succeed").rating,
ClipRating::Ok
);
}
#[test]
fn test_export_csv() {
let mut db = RatingDatabase::new();
db.set_rating(1, ClipRating::Good);
db.set_flag(1, ClipFlag::Green);
let csv = export_csv(&db);
assert!(csv.starts_with("clip_id,rating,flag,notes"));
assert!(csv.contains("Good"));
assert!(csv.contains("Green"));
}
#[test]
fn test_batch_result() {
let r = BatchResult::new(5, 2);
assert_eq!(r.processed, 5);
assert_eq!(r.skipped, 2);
}
}