Skip to main content

use_db_record/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Record, document, and entity metadata primitives for `RustUse`.
5
6use core::fmt;
7use std::error::Error;
8
9macro_rules! record_text_type {
10    ($type_name:ident) => {
11        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12        pub struct $type_name(String);
13
14        impl $type_name {
15            /// Creates a non-empty record metadata label.
16            ///
17            /// # Errors
18            ///
19            /// Returns [`RecordError`] when the label is empty or contains control characters.
20            pub fn new(input: impl AsRef<str>) -> Result<Self, RecordError> {
21                validate_text(input.as_ref()).map(|value| Self(value.to_owned()))
22            }
23
24            /// Returns the stored label.
25            #[must_use]
26            pub fn as_str(&self) -> &str {
27                &self.0
28            }
29        }
30
31        impl fmt::Display for $type_name {
32            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33                formatter.write_str(self.as_str())
34            }
35        }
36    };
37}
38
39record_text_type!(RecordId);
40record_text_type!(RecordKey);
41
42/// A monotonically meaningful record version value.
43#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
44pub struct RecordVersion(u64);
45
46impl RecordVersion {
47    /// Creates a record version.
48    #[must_use]
49    pub const fn new(value: u64) -> Self {
50        Self(value)
51    }
52
53    /// Returns the version value.
54    #[must_use]
55    pub const fn value(self) -> u64 {
56        self.0
57    }
58}
59
60/// Broad record status.
61#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
62pub enum RecordStatus {
63    /// Record is active/current.
64    #[default]
65    Active,
66    /// Record is soft-deleted or removed.
67    Deleted,
68    /// Record is archived.
69    Archived,
70    /// Status is unknown.
71    Unknown,
72}
73
74/// A generic record reference.
75#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
76pub struct RecordRef {
77    id: RecordId,
78    key: Option<RecordKey>,
79}
80
81impl RecordRef {
82    /// Creates a record reference from an id.
83    #[must_use]
84    pub const fn new(id: RecordId) -> Self {
85        Self { id, key: None }
86    }
87
88    /// Adds a record key.
89    #[must_use]
90    pub fn with_key(mut self, key: RecordKey) -> Self {
91        self.key = Some(key);
92        self
93    }
94
95    /// Returns the record id.
96    #[must_use]
97    pub const fn id(&self) -> &RecordId {
98        &self.id
99    }
100
101    /// Returns the optional record key.
102    #[must_use]
103    pub const fn key(&self) -> Option<&RecordKey> {
104        self.key.as_ref()
105    }
106}
107
108/// Error returned by record metadata constructors.
109#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110pub enum RecordError {
111    /// Text was empty.
112    Empty,
113    /// Text contained a control character.
114    ControlCharacter,
115}
116
117impl fmt::Display for RecordError {
118    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119        match self {
120            Self::Empty => formatter.write_str("record label cannot be empty"),
121            Self::ControlCharacter => {
122                formatter.write_str("record label cannot contain control characters")
123            },
124        }
125    }
126}
127
128impl Error for RecordError {}
129
130fn validate_text(input: &str) -> Result<&str, RecordError> {
131    if input.chars().any(char::is_control) {
132        return Err(RecordError::ControlCharacter);
133    }
134    let trimmed = input.trim();
135    if trimmed.is_empty() {
136        return Err(RecordError::Empty);
137    }
138    Ok(trimmed)
139}
140
141#[cfg(test)]
142mod tests {
143    use super::{RecordError, RecordId, RecordKey, RecordRef, RecordStatus, RecordVersion};
144
145    #[test]
146    fn stores_record_metadata() -> Result<(), RecordError> {
147        let reference = RecordRef::new(RecordId::new("42")?).with_key(RecordKey::new("users/42")?);
148        let version = RecordVersion::new(7);
149
150        assert_eq!(reference.id().as_str(), "42");
151        assert_eq!(reference.key().expect("key").as_str(), "users/42");
152        assert_eq!(version.value(), 7);
153        assert_eq!(RecordStatus::default(), RecordStatus::Active);
154        Ok(())
155    }
156}