#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
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 {
pub fn new(input: impl AsRef<str>) -> Result<Self, RecordError> {
validate_text(input.as_ref()).map(|value| Self(value.to_owned()))
}
#[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);
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RecordVersion(u64);
impl RecordVersion {
#[must_use]
pub const fn new(value: u64) -> Self {
Self(value)
}
#[must_use]
pub const fn value(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum RecordStatus {
#[default]
Active,
Deleted,
Archived,
Unknown,
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RecordRef {
id: RecordId,
key: Option<RecordKey>,
}
impl RecordRef {
#[must_use]
pub const fn new(id: RecordId) -> Self {
Self { id, key: None }
}
#[must_use]
pub fn with_key(mut self, key: RecordKey) -> Self {
self.key = Some(key);
self
}
#[must_use]
pub const fn id(&self) -> &RecordId {
&self.id
}
#[must_use]
pub const fn key(&self) -> Option<&RecordKey> {
self.key.as_ref()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RecordError {
Empty,
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(())
}
}