use-db-record 0.1.0

Primitive database record metadata for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

//! Record, document, and entity metadata primitives for `RustUse`.

use core::fmt;
use std::error::Error;

macro_rules! record_text_type {
    ($type_name:ident) => {
        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
        pub struct $type_name(String);

        impl $type_name {
            /// Creates a non-empty record metadata label.
            ///
            /// # Errors
            ///
            /// Returns [`RecordError`] when the label is empty or contains control characters.
            pub fn new(input: impl AsRef<str>) -> Result<Self, RecordError> {
                validate_text(input.as_ref()).map(|value| Self(value.to_owned()))
            }

            /// Returns the stored label.
            #[must_use]
            pub fn as_str(&self) -> &str {
                &self.0
            }
        }

        impl fmt::Display for $type_name {
            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter.write_str(self.as_str())
            }
        }
    };
}

record_text_type!(RecordId);
record_text_type!(RecordKey);

/// A monotonically meaningful record version value.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RecordVersion(u64);

impl RecordVersion {
    /// Creates a record version.
    #[must_use]
    pub const fn new(value: u64) -> Self {
        Self(value)
    }

    /// Returns the version value.
    #[must_use]
    pub const fn value(self) -> u64 {
        self.0
    }
}

/// Broad record status.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum RecordStatus {
    /// Record is active/current.
    #[default]
    Active,
    /// Record is soft-deleted or removed.
    Deleted,
    /// Record is archived.
    Archived,
    /// Status is unknown.
    Unknown,
}

/// A generic record reference.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RecordRef {
    id: RecordId,
    key: Option<RecordKey>,
}

impl RecordRef {
    /// Creates a record reference from an id.
    #[must_use]
    pub const fn new(id: RecordId) -> Self {
        Self { id, key: None }
    }

    /// Adds a record key.
    #[must_use]
    pub fn with_key(mut self, key: RecordKey) -> Self {
        self.key = Some(key);
        self
    }

    /// Returns the record id.
    #[must_use]
    pub const fn id(&self) -> &RecordId {
        &self.id
    }

    /// Returns the optional record key.
    #[must_use]
    pub const fn key(&self) -> Option<&RecordKey> {
        self.key.as_ref()
    }
}

/// Error returned by record metadata constructors.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RecordError {
    /// Text was empty.
    Empty,
    /// Text contained a control character.
    ControlCharacter,
}

impl fmt::Display for RecordError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => formatter.write_str("record label cannot be empty"),
            Self::ControlCharacter => {
                formatter.write_str("record label cannot contain control characters")
            },
        }
    }
}

impl Error for RecordError {}

fn validate_text(input: &str) -> Result<&str, RecordError> {
    if input.chars().any(char::is_control) {
        return Err(RecordError::ControlCharacter);
    }
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return Err(RecordError::Empty);
    }
    Ok(trimmed)
}

#[cfg(test)]
mod tests {
    use super::{RecordError, RecordId, RecordKey, RecordRef, RecordStatus, RecordVersion};

    #[test]
    fn stores_record_metadata() -> Result<(), RecordError> {
        let reference = RecordRef::new(RecordId::new("42")?).with_key(RecordKey::new("users/42")?);
        let version = RecordVersion::new(7);

        assert_eq!(reference.id().as_str(), "42");
        assert_eq!(reference.key().expect("key").as_str(), "users/42");
        assert_eq!(version.value(), 7);
        assert_eq!(RecordStatus::default(), RecordStatus::Active);
        Ok(())
    }
}