use derive_more::{IsVariant, TryUnwrap, Unwrap};
use jiff::Timestamp;
use crate::domain::{ErrorInfo, ScanStatus, Uuid7};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct WatchedLocation<Id = Uuid7> {
id: Id,
volume: Id,
recursive: bool,
enabled: bool,
is_ejectable: bool,
added_at: Timestamp,
last_reconciled_at: Option<Timestamp>,
last_reconcile_status: Option<ScanStatus>,
last_error: Option<ErrorInfo>,
}
impl WatchedLocation<Uuid7> {
pub fn try_new(
id: Uuid7,
volume: Uuid7,
added_at: Timestamp,
) -> Result<Self, WatchedLocationError> {
if id.is_nil() {
return Err(WatchedLocationError::NilId);
}
if volume.is_nil() {
return Err(WatchedLocationError::NilVolume);
}
Ok(Self {
id,
volume,
recursive: false,
enabled: false,
is_ejectable: false,
added_at,
last_reconciled_at: None,
last_reconcile_status: None,
last_error: None,
})
}
}
impl<Id> WatchedLocation<Id> {
#[inline(always)]
pub const fn id_ref(&self) -> &Id {
&self.id
}
#[inline(always)]
pub const fn volume_ref(&self) -> &Id {
&self.volume
}
#[inline(always)]
pub const fn is_recursive(&self) -> bool {
self.recursive
}
#[inline(always)]
pub const fn is_enabled(&self) -> bool {
self.enabled
}
#[inline(always)]
pub const fn is_ejectable(&self) -> bool {
self.is_ejectable
}
#[inline(always)]
pub const fn added_at_ref(&self) -> &Timestamp {
&self.added_at
}
#[inline(always)]
pub const fn last_reconciled_at_ref(&self) -> Option<&Timestamp> {
self.last_reconciled_at.as_ref()
}
#[inline(always)]
pub const fn last_reconcile_status_ref(&self) -> Option<&ScanStatus> {
self.last_reconcile_status.as_ref()
}
#[inline(always)]
pub const fn last_error_ref(&self) -> Option<&ErrorInfo> {
self.last_error.as_ref()
}
#[inline(always)]
#[must_use]
pub const fn with_recursive(mut self, recursive: bool) -> Self {
self.recursive = recursive;
self
}
#[inline(always)]
#[must_use]
pub const fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
#[inline(always)]
#[must_use]
pub const fn with_ejectable(mut self, is_ejectable: bool) -> Self {
self.is_ejectable = is_ejectable;
self
}
#[inline(always)]
#[must_use]
pub fn with_last_reconciled_at(mut self, t: Option<Timestamp>) -> Self {
self.last_reconciled_at = t;
self
}
#[inline(always)]
#[must_use]
pub fn with_last_reconcile_status(mut self, s: Option<ScanStatus>) -> Self {
self.last_reconcile_status = s;
self
}
#[inline(always)]
#[must_use]
pub fn with_last_error(mut self, e: Option<ErrorInfo>) -> Self {
self.last_error = e;
self
}
#[inline(always)]
pub const fn set_recursive(&mut self, recursive: bool) -> &mut Self {
self.recursive = recursive;
self
}
#[inline(always)]
pub const fn set_enabled(&mut self, enabled: bool) -> &mut Self {
self.enabled = enabled;
self
}
#[inline(always)]
pub const fn set_ejectable(&mut self, is_ejectable: bool) -> &mut Self {
self.is_ejectable = is_ejectable;
self
}
#[inline(always)]
pub fn set_last_reconciled_at(&mut self, t: Option<Timestamp>) -> &mut Self {
self.last_reconciled_at = t;
self
}
#[inline(always)]
pub fn set_last_reconcile_status(&mut self, s: Option<ScanStatus>) -> &mut Self {
self.last_reconcile_status = s;
self
}
#[inline(always)]
pub fn set_last_error(&mut self, e: Option<ErrorInfo>) -> &mut Self {
self.last_error = e;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, IsVariant, Unwrap, TryUnwrap, thiserror::Error)]
#[unwrap(ref, ref_mut)]
#[try_unwrap(ref, ref_mut)]
#[non_exhaustive]
pub enum WatchedLocationError {
#[error("WatchedLocation id must not be the nil UUID")]
NilId,
#[error("WatchedLocation volume must not be the nil UUID")]
NilVolume,
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use crate::domain::ErrorCode;
#[test]
fn try_new_happy_path() {
let id = Uuid7::new();
let vol = Uuid7::new();
let w = WatchedLocation::try_new(id, vol, Timestamp::default())
.expect("valid construction must succeed");
assert_eq!(w.id_ref(), &id);
assert_eq!(w.volume_ref(), &vol);
assert!(!w.is_enabled(), "monitor starts paused");
assert!(!w.is_recursive());
assert!(!w.is_ejectable());
}
#[test]
fn try_new_rejects_nil_id() {
let r = WatchedLocation::try_new(Uuid7::nil(), Uuid7::new(), Timestamp::default());
assert_eq!(r.err(), Some(WatchedLocationError::NilId));
assert!(WatchedLocationError::NilId.is_nil_id());
}
#[test]
fn try_new_rejects_nil_volume() {
let r = WatchedLocation::try_new(Uuid7::new(), Uuid7::nil(), Timestamp::default());
assert_eq!(r.err(), Some(WatchedLocationError::NilVolume));
assert!(WatchedLocationError::NilVolume.is_nil_volume());
}
#[test]
fn enabling_a_removable_drive_watch() {
let vol = Uuid7::new();
let w = WatchedLocation::try_new(Uuid7::new(), vol, Timestamp::default())
.unwrap()
.with_recursive(true)
.with_enabled(true)
.with_ejectable(true);
assert!(w.is_ejectable() && w.is_recursive() && w.is_enabled());
}
#[test]
fn non_ejectable_records_volume_unavailable_error() {
let w = WatchedLocation::try_new(Uuid7::new(), Uuid7::new(), Timestamp::default())
.unwrap()
.with_last_error(Some(ErrorInfo::new(
ErrorCode::VolumeNotAvailable,
"drive offline",
)));
assert_eq!(
w.last_error_ref().map(|e| e.code()),
Some(ErrorCode::VolumeNotAvailable)
);
}
#[test]
fn setters_mutate_in_place() {
let mut w = WatchedLocation::try_new(Uuid7::new(), Uuid7::new(), Timestamp::default()).unwrap();
w.set_enabled(true);
w.set_recursive(true);
w.set_ejectable(true);
assert!(w.is_enabled() && w.is_recursive() && w.is_ejectable());
}
}