simplelock 0.4.1

Simple abstractions for inter-process synchronization.
Documentation
use crate::*;

const DEFAULT_TRY_LOCK: bool = false;
const DEFAULT_SILENT_UNLOCK: bool = false;

/// A Lock implementation which wraps a ConcreteLock to call hang_lock.
struct HangLock(Box<dyn ConcreteLock>);
impl Lock for HangLock {
    fn status(&self) -> SimpleLockResult<LockStatus> {
        self.0.status()
    }

    fn lock(&mut self) -> SimpleLockResult<()> {
        self.0.hang_lock()
    }

    fn unlock(&mut self) -> SimpleLockResult<()> {
        self.0.try_unlock()
    }
}

/// A Lock implementation which wraps a ConcreteLock to call try_lock.
struct TryLock(Box<dyn ConcreteLock>);
impl Lock for TryLock {
    fn status(&self) -> SimpleLockResult<LockStatus> {
        self.0.status()
    }

    fn lock(&mut self) -> SimpleLockResult<()> {
        self.0.try_lock()
    }

    fn unlock(&mut self) -> SimpleLockResult<()> {
        self.0.try_unlock()
    }
}

/// A Lock implementation which masks any unlock failure.
struct SilentUnlocker(Box<dyn Lock>);
impl Lock for SilentUnlocker {
    fn status(&self) -> SimpleLockResult<LockStatus> {
        self.0.status()
    }

    fn lock(&mut self) -> SimpleLockResult<()> {
        self.0.lock()
    }

    fn unlock(&mut self) -> SimpleLockResult<()> {
        // TODO: Can we save the error in the Unlocker so that a cast can extract it?
        let _ = self.0.unlock();
        Ok(())
    }
}

/// Builds a lock from a ConcreteLock implementation using the
/// configurations enabled. See below for all possible
/// configurations. Note, by implementing the ConcreteLock
/// implementation you can wrap it with a LockBuilder to create
/// the Lock interface used in all utility functions.
#[derive(Copy, Clone, Debug)]
pub struct LockBuilder {
    /// When attempting to lock, if we are unable to immediately
    /// acquire the lock, what should we do? If `false', we will
    /// hang indefinitely until we are able to acquire the lock.
    /// If `true', we will return immediately with
    /// "SimpleLockError::AlreadyLocked".
    try_lock: bool, // default 'false'

    /// When attempting to unlock, if we fail, how should we react?
    /// If `true' we will continue silently and return a result. It
    /// is up to the caller to confirm the lock status with
    /// `Lock::status`. If `false', we will return with an error
    /// "UnableToUnlock(...)" with an embedded error as the reason.
    silent_unlock: bool, // default 'false'
}

impl Default for LockBuilder {
    fn default() -> Self {
        LockBuilder {
            try_lock: DEFAULT_TRY_LOCK,
            silent_unlock: DEFAULT_SILENT_UNLOCK,
        }
    }
}

impl LockBuilder {
    /// If the unlock fails, intermediate computation may be lost. This may not be
    /// the behaviour you wished for, so enable silent_unlock. You can then check
    /// the lock status afterwards by performing Lock::status.
    pub fn enable_silent_unlock(mut self) {
        self.silent_unlock = true;
    }

    /// If the lock is already taken, by default we would hang until it was our turn.
    /// By enabling 'try_lock' we instead try to lock but fail immediately with
    /// SimpleLockError::AlreadyLocked if we could not acquire it.
    pub fn enable_try_lock(mut self) {
        self.try_lock = true;
    }

    /// Performs `enable_status_unlock` and then returns self so that we can chain
    /// additional configurations or a call to `build`.
    pub fn with_silent_unlock(self) -> Self {
        self.enable_silent_unlock();
        self
    }

    /// Performs `enable_try_lock` and then returns self so that we can chain
    /// additional configurations or a call to `build`.
    pub fn with_try_lock(self) -> Self {
        self.enable_try_lock();
        self
    }

    /// Wraps a ConcreteLock with the configurations enabled with this LockBuilder and
    /// returns the simplified Lock interface.
    pub fn wrap(&self, concrete: Box<dyn ConcreteLock>) -> SimpleLockResult<Box<dyn Lock>> {
        let lock: Box<dyn Lock> = if self.try_lock {
            Box::new(TryLock(concrete))
        } else {
            Box::new(HangLock(concrete))
        };

        if self.silent_unlock {
            Ok(Box::new(SilentUnlocker(lock)))
        } else {
            Ok(lock)
        }
    }

    /// Create a Lock based on the configurations enabled using the implementation
    /// enabled in the Cargo.toml. For example, if the "file" feature is enabled
    /// a FileLock will be produced from this build call.
    #[allow(unused_variables)]
    pub fn build(&self, app_name: impl Into<String>) -> SimpleLockResult<Box<dyn Lock>> {
        cfg_if! {
            // Placing "file" locks as first pick due to cross-platform utility.
            if #[cfg(feature = "file")] {
                let path = format!("/tmp/lock-{}", app_name.into());
                self.wrap(Box::new(FileLock::new(path)?))

            } else if #[cfg(feature = "sema")] {
                self.wrap(Box::new(SemaphoreLock::new(app_name)?))

            // Order sort of matters here. Placing "fake" and "none" at the bottom
            // so that it prioritizes real IPC locks if more than one feature is enabled.
            } else if #[cfg(feature = "fake")] {
                Ok(Box::new(FakeLock::default()))
            } else {
                Err(SimpleLockError::NoLockEnabled)
            }
        }
    }
}