use std::collections::BTreeMap;
use crate::MultiUuError;
#[derive(Clone)]
pub struct PartEntry {
pub part_number: u32,
pub body_bytes: Vec<u8>,
pub subject: Option<String>,
}
#[derive(Clone)]
pub struct PartCollection {
parts: BTreeMap<u32, PartEntry>,
total: Option<u32>,
}
impl PartCollection {
pub fn new() -> Self {
Self {
parts: BTreeMap::new(),
total: None,
}
}
pub fn with_total(total: u32) -> Self {
Self {
parts: BTreeMap::new(),
total: Some(total),
}
}
pub fn add(&mut self, entry: PartEntry) -> Result<(), MultiUuError> {
let pn = entry.part_number;
if self.parts.contains_key(&pn) {
return Err(MultiUuError::DuplicatePart { part_number: pn });
}
if pn > 0 {
self.total = Some(match self.total {
Some(t) => t.max(pn),
None => pn,
});
}
self.parts.insert(pn, entry);
Ok(())
}
pub fn total(&self) -> Option<u32> {
self.total
}
pub fn present_parts(&self) -> impl Iterator<Item = u32> + '_ {
self.parts.keys().copied()
}
pub fn missing_parts(&self) -> Vec<u32> {
match self.total {
None => vec![],
Some(t) => (1..=t).filter(|n| !self.parts.contains_key(n)).collect(),
}
}
pub fn is_complete(&self) -> bool {
match self.total {
None => false,
Some(_) => self.missing_parts().is_empty(),
}
}
pub fn toc_part(&self) -> Option<&PartEntry> {
self.parts.get(&0)
}
pub fn get(&self, part_number: u32) -> Option<&PartEntry> {
self.parts.get(&part_number)
}
pub fn len(&self) -> usize {
self.parts.len()
}
pub fn is_empty(&self) -> bool {
self.parts.is_empty()
}
}
impl Default for PartCollection {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn part(n: u32) -> PartEntry {
PartEntry {
part_number: n,
body_bytes: vec![],
subject: None,
}
}
#[test]
fn out_of_order_insertion_sorted() {
let mut c = PartCollection::new();
c.add(PartEntry {
part_number: 3,
body_bytes: vec![],
subject: None,
})
.unwrap();
c.add(PartEntry {
part_number: 1,
body_bytes: vec![],
subject: None,
})
.unwrap();
let got: Vec<u32> = c.present_parts().collect();
assert_eq!(got, vec![1, 3]);
}
#[test]
fn gap_detection() {
let mut c = PartCollection::with_total(4);
c.add(part(1)).unwrap();
c.add(part(2)).unwrap();
c.add(part(4)).unwrap();
assert_eq!(c.missing_parts(), vec![3]);
}
#[test]
fn duplicate_returns_error() {
let mut c = PartCollection::new();
c.add(part(1)).unwrap();
assert!(matches!(
c.add(part(1)),
Err(MultiUuError::DuplicatePart { part_number: 1 })
));
}
#[test]
fn is_complete_when_all_present() {
let mut c = PartCollection::with_total(2);
c.add(part(1)).unwrap();
c.add(part(2)).unwrap();
assert!(c.is_complete());
}
#[test]
fn is_complete_false_when_total_unknown() {
let mut c = PartCollection::new();
c.add(part(1)).unwrap();
let mut c2 = PartCollection::new();
c2.add(part(1)).unwrap();
c2.add(part(3)).unwrap(); assert!(!c2.is_complete());
}
#[test]
fn toc_part_returned() {
let mut c = PartCollection::new();
c.add(PartEntry {
part_number: 0,
body_bytes: b"toc".to_vec(),
subject: None,
})
.unwrap();
assert!(c.toc_part().is_some());
}
#[test]
fn len_and_is_empty() {
let mut c = PartCollection::new();
assert!(c.is_empty());
assert_eq!(c.len(), 0);
c.add(part(1)).unwrap();
assert!(!c.is_empty());
assert_eq!(c.len(), 1);
}
#[test]
fn missing_parts_empty_when_no_total() {
let c = PartCollection::new();
assert_eq!(c.missing_parts(), vec![] as Vec<u32>);
}
#[test]
fn with_total_sets_total() {
let c = PartCollection::with_total(5);
assert_eq!(c.total(), Some(5));
}
#[test]
fn add_bumps_total_upward() {
let mut c = PartCollection::with_total(3);
c.add(part(5)).unwrap(); assert_eq!(c.total(), Some(5));
}
#[test]
fn toc_does_not_affect_total() {
let mut c = PartCollection::new();
c.add(PartEntry {
part_number: 0,
body_bytes: vec![],
subject: None,
})
.unwrap();
assert_eq!(c.total(), None);
}
#[test]
fn default_is_same_as_new() {
let c: PartCollection = Default::default();
assert!(c.is_empty());
assert_eq!(c.total(), None);
}
}