kcode-k1-web-source-object 0.1.0

Preserve and capture canonical K1 Web source-package objects
Documentation
//! Capture canonical K1 Web source packages as Objects transactions.

use std::fmt;
use std::sync::Mutex;

use kcode_k1_objects::{K1Objects, TxId};
use kcode_k1_web_package::SourcePackage;

/// The file type used for preserved source-package objects.
const SOURCE_PACKAGE_FILE_TYPE: &str = "k1-web-source-package-v1";

/// Captures the Objects transaction created while preserving a source package.
pub struct SourceObjectCapture<'a> {
    objects: &'a K1Objects,
    captured: Mutex<Option<TxId>>,
}

impl<'a> SourceObjectCapture<'a> {
    /// Creates an empty capture associated with `objects`.
    pub fn new(objects: &'a K1Objects) -> Self {
        Self {
            objects,
            captured: Mutex::new(None),
        }
    }

    /// Encodes and saves `package`, recording the successful transaction ID.
    pub fn preserve(&self, package: &SourcePackage) -> Result<(), String> {
        let bytes = kcode_k1_web_transaction::encode(package).map_err(|error| error.to_string())?;
        let mut captured = self
            .captured
            .lock()
            .map_err(|_| CaptureStateError::Poisoned.to_string())?;
        let id = self
            .objects
            .save("", SOURCE_PACKAGE_FILE_TYPE, "", &bytes)?;
        *captured = Some(id);
        Ok(())
    }

    /// Returns the captured transaction ID, or `None` if absent or poisoned.
    pub fn captured(&self) -> Option<TxId> {
        self.captured.lock().ok().and_then(|captured| *captured)
    }

    /// Consumes the capture and returns its recorded transaction ID.
    pub fn finish(self) -> Result<TxId, CaptureStateError> {
        self.captured
            .into_inner()
            .map_err(|_| CaptureStateError::Poisoned)?
            .ok_or(CaptureStateError::Missing)
    }
}

/// Failure to recover a captured source-object transaction ID.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CaptureStateError {
    /// The capture mutex was poisoned.
    Poisoned,
    /// Publication completed without capturing a source object.
    Missing,
}

impl fmt::Display for CaptureStateError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(match self {
            Self::Poisoned => "source object capture mutex poisoned",
            Self::Missing => "publication completed without a captured source object",
        })
    }
}

impl std::error::Error for CaptureStateError {}

#[cfg(test)]
mod tests {
    use super::CaptureStateError;

    #[test]
    fn capture_state_errors_have_stable_messages() {
        assert_eq!(
            CaptureStateError::Poisoned.to_string(),
            "source object capture mutex poisoned"
        );
        assert_eq!(
            CaptureStateError::Missing.to_string(),
            "publication completed without a captured source object"
        );
    }
}