1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use 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 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 #[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#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
44pub struct RecordVersion(u64);
45
46impl RecordVersion {
47 #[must_use]
49 pub const fn new(value: u64) -> Self {
50 Self(value)
51 }
52
53 #[must_use]
55 pub const fn value(self) -> u64 {
56 self.0
57 }
58}
59
60#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
62pub enum RecordStatus {
63 #[default]
65 Active,
66 Deleted,
68 Archived,
70 Unknown,
72}
73
74#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
76pub struct RecordRef {
77 id: RecordId,
78 key: Option<RecordKey>,
79}
80
81impl RecordRef {
82 #[must_use]
84 pub const fn new(id: RecordId) -> Self {
85 Self { id, key: None }
86 }
87
88 #[must_use]
90 pub fn with_key(mut self, key: RecordKey) -> Self {
91 self.key = Some(key);
92 self
93 }
94
95 #[must_use]
97 pub const fn id(&self) -> &RecordId {
98 &self.id
99 }
100
101 #[must_use]
103 pub const fn key(&self) -> Option<&RecordKey> {
104 self.key.as_ref()
105 }
106}
107
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110pub enum RecordError {
111 Empty,
113 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}