Skip to main content

use_document_store/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5
6macro_rules! string_newtype {
7    ($(#[$meta:meta])* $name:ident) => {
8        $(#[$meta])*
9        #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
10        pub struct $name(String);
11
12        impl $name {
13            /// Creates a new string-backed primitive.
14            pub fn new(value: impl Into<String>) -> Self {
15                Self(value.into())
16            }
17
18            /// Returns the stored string value.
19            pub fn as_str(&self) -> &str {
20                &self.0
21            }
22        }
23
24        impl AsRef<str> for $name {
25            fn as_ref(&self) -> &str {
26                self.as_str()
27            }
28        }
29
30        impl From<String> for $name {
31            fn from(value: String) -> Self {
32                Self::new(value)
33            }
34        }
35
36        impl From<&str> for $name {
37            fn from(value: &str) -> Self {
38                Self::new(value)
39            }
40        }
41
42        impl fmt::Display for $name {
43            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44                formatter.write_str(self.as_str())
45            }
46        }
47    };
48}
49
50string_newtype! {
51    /// A document collection name.
52    CollectionName
53}
54string_newtype! {
55    /// A document identifier.
56    DocumentId
57}
58string_newtype! {
59    /// A document field label.
60    DocumentField
61}
62
63/// A monotonically increasing document revision label.
64#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
65pub struct DocumentRevision(u64);
66
67impl DocumentRevision {
68    /// Creates a document revision value.
69    pub const fn new(value: u64) -> Self {
70        Self(value)
71    }
72
73    /// Returns the revision value.
74    pub const fn value(self) -> u64 {
75        self.0
76    }
77}
78
79impl fmt::Display for DocumentRevision {
80    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81        write!(formatter, "{}", self.0)
82    }
83}
84
85/// A document schema or application version label.
86#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
87pub struct DocumentVersion(u64);
88
89impl DocumentVersion {
90    /// Creates a document version value.
91    pub const fn new(value: u64) -> Self {
92        Self(value)
93    }
94
95    /// Returns the version value.
96    pub const fn value(self) -> u64 {
97        self.0
98    }
99}
100
101impl fmt::Display for DocumentVersion {
102    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103        write!(formatter, "{}", self.0)
104    }
105}
106
107/// Vendor-neutral metadata for a modeled document.
108#[derive(Clone, Debug, Default, Eq, PartialEq)]
109pub struct DocumentMetadata {
110    collection: Option<CollectionName>,
111    revision: Option<DocumentRevision>,
112    version: Option<DocumentVersion>,
113}
114
115impl DocumentMetadata {
116    /// Creates empty document metadata.
117    pub const fn new() -> Self {
118        Self {
119            collection: None,
120            revision: None,
121            version: None,
122        }
123    }
124
125    /// Sets the collection name.
126    pub fn with_collection(mut self, collection: CollectionName) -> Self {
127        self.collection = Some(collection);
128        self
129    }
130
131    /// Sets the document revision.
132    pub const fn with_revision(mut self, revision: DocumentRevision) -> Self {
133        self.revision = Some(revision);
134        self
135    }
136
137    /// Sets the document version.
138    pub const fn with_version(mut self, version: DocumentVersion) -> Self {
139        self.version = Some(version);
140        self
141    }
142
143    /// Returns the collection name, if present.
144    pub const fn collection(&self) -> Option<&CollectionName> {
145        self.collection.as_ref()
146    }
147
148    /// Returns the revision, if present.
149    pub const fn revision(&self) -> Option<DocumentRevision> {
150        self.revision
151    }
152
153    /// Returns the version, if present.
154    pub const fn version(&self) -> Option<DocumentVersion> {
155        self.version
156    }
157}
158
159/// A single document patch operation.
160#[derive(Clone, Debug, Eq, Hash, PartialEq)]
161pub enum PatchOperation {
162    Set { path: String, value: String },
163    Unset { path: String },
164    Remove { path: String },
165    Increment { path: String, amount: i64 },
166    Append { path: String, value: String },
167    Prepend { path: String, value: String },
168    Merge { path: String, value: String },
169    Replace { path: String, value: String },
170}
171
172impl PatchOperation {
173    /// Creates a set operation.
174    pub fn set(path: impl AsRef<str>, value: impl Into<String>) -> Self {
175        Self::Set {
176            path: path.as_ref().to_owned(),
177            value: value.into(),
178        }
179    }
180
181    /// Creates an unset operation.
182    pub fn unset(path: impl AsRef<str>) -> Self {
183        Self::Unset {
184            path: path.as_ref().to_owned(),
185        }
186    }
187
188    /// Creates a remove operation.
189    pub fn remove(path: impl AsRef<str>) -> Self {
190        Self::Remove {
191            path: path.as_ref().to_owned(),
192        }
193    }
194
195    /// Creates an increment operation.
196    pub fn increment(path: impl AsRef<str>, amount: i64) -> Self {
197        Self::Increment {
198            path: path.as_ref().to_owned(),
199            amount,
200        }
201    }
202
203    /// Creates an append operation.
204    pub fn append(path: impl AsRef<str>, value: impl Into<String>) -> Self {
205        Self::Append {
206            path: path.as_ref().to_owned(),
207            value: value.into(),
208        }
209    }
210
211    /// Creates a prepend operation.
212    pub fn prepend(path: impl AsRef<str>, value: impl Into<String>) -> Self {
213        Self::Prepend {
214            path: path.as_ref().to_owned(),
215            value: value.into(),
216        }
217    }
218
219    /// Creates a merge operation.
220    pub fn merge(path: impl AsRef<str>, value: impl Into<String>) -> Self {
221        Self::Merge {
222            path: path.as_ref().to_owned(),
223            value: value.into(),
224        }
225    }
226
227    /// Creates a replace operation.
228    pub fn replace(path: impl AsRef<str>, value: impl Into<String>) -> Self {
229        Self::Replace {
230            path: path.as_ref().to_owned(),
231            value: value.into(),
232        }
233    }
234
235    /// Returns the operation path.
236    pub fn path(&self) -> &str {
237        match self {
238            Self::Set { path, .. }
239            | Self::Unset { path }
240            | Self::Remove { path }
241            | Self::Increment { path, .. }
242            | Self::Append { path, .. }
243            | Self::Prepend { path, .. }
244            | Self::Merge { path, .. }
245            | Self::Replace { path, .. } => path,
246        }
247    }
248}
249
250/// A set of document patch operations.
251#[derive(Clone, Debug, Default, Eq, PartialEq)]
252pub struct PatchSet {
253    operations: Vec<PatchOperation>,
254}
255
256impl PatchSet {
257    /// Creates a patch set from operations.
258    pub fn new(operations: Vec<PatchOperation>) -> Self {
259        Self { operations }
260    }
261
262    /// Returns the patch operations.
263    pub fn operations(&self) -> &[PatchOperation] {
264        &self.operations
265    }
266
267    /// Adds one operation.
268    pub fn with_operation(mut self, operation: PatchOperation) -> Self {
269        self.operations.push(operation);
270        self
271    }
272}
273
274/// Patch operations addressed to one document.
275#[derive(Clone, Debug, Eq, PartialEq)]
276pub struct DocumentPatch {
277    document_id: DocumentId,
278    patch_set: PatchSet,
279}
280
281impl DocumentPatch {
282    /// Creates a document patch.
283    pub fn new(document_id: DocumentId, patch_set: PatchSet) -> Self {
284        Self {
285            document_id,
286            patch_set,
287        }
288    }
289
290    /// Returns the patched document identifier.
291    pub const fn document_id(&self) -> &DocumentId {
292        &self.document_id
293    }
294
295    /// Returns the patch set.
296    pub const fn patch_set(&self) -> &PatchSet {
297        &self.patch_set
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::{
304        CollectionName, DocumentId, DocumentMetadata, DocumentPatch, DocumentRevision,
305        DocumentVersion, PatchOperation, PatchSet,
306    };
307    use std::collections::hash_map::DefaultHasher;
308    use std::hash::{Hash, Hasher};
309
310    #[test]
311    fn constructs_document_newtypes() {
312        let collection = CollectionName::new("customers");
313        let document_id = DocumentId::new("customer_123");
314        assert_eq!(collection.to_string(), "customers");
315        assert_eq!(document_id.as_ref(), "customer_123");
316    }
317
318    #[test]
319    fn hashes_equal_document_ids() {
320        let mut left = DefaultHasher::new();
321        let mut right = DefaultHasher::new();
322        DocumentId::new("same").hash(&mut left);
323        DocumentId::new("same").hash(&mut right);
324        assert_eq!(left.finish(), right.finish());
325    }
326
327    #[test]
328    fn builds_metadata_and_patch_operations() {
329        let metadata = DocumentMetadata::new()
330            .with_collection(CollectionName::new("customers"))
331            .with_revision(DocumentRevision::new(7))
332            .with_version(DocumentVersion::new(2));
333        let patch = PatchSet::new(vec![
334            PatchOperation::set("profile.display_name", "Joshua Whalen"),
335            PatchOperation::increment("stats.reviews", 1),
336        ]);
337        let document_patch = DocumentPatch::new(DocumentId::new("customer_123"), patch);
338
339        assert_eq!(metadata.collection().unwrap().as_str(), "customers");
340        assert_eq!(metadata.revision(), Some(DocumentRevision::new(7)));
341        assert_eq!(
342            document_patch.patch_set().operations()[0].path(),
343            "profile.display_name"
344        );
345    }
346}