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 pub fn new(value: impl Into<String>) -> Self {
15 Self(value.into())
16 }
17
18 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 CollectionName
53}
54string_newtype! {
55 DocumentId
57}
58string_newtype! {
59 DocumentField
61}
62
63#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
65pub struct DocumentRevision(u64);
66
67impl DocumentRevision {
68 pub const fn new(value: u64) -> Self {
70 Self(value)
71 }
72
73 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#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
87pub struct DocumentVersion(u64);
88
89impl DocumentVersion {
90 pub const fn new(value: u64) -> Self {
92 Self(value)
93 }
94
95 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#[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 pub const fn new() -> Self {
118 Self {
119 collection: None,
120 revision: None,
121 version: None,
122 }
123 }
124
125 pub fn with_collection(mut self, collection: CollectionName) -> Self {
127 self.collection = Some(collection);
128 self
129 }
130
131 pub const fn with_revision(mut self, revision: DocumentRevision) -> Self {
133 self.revision = Some(revision);
134 self
135 }
136
137 pub const fn with_version(mut self, version: DocumentVersion) -> Self {
139 self.version = Some(version);
140 self
141 }
142
143 pub const fn collection(&self) -> Option<&CollectionName> {
145 self.collection.as_ref()
146 }
147
148 pub const fn revision(&self) -> Option<DocumentRevision> {
150 self.revision
151 }
152
153 pub const fn version(&self) -> Option<DocumentVersion> {
155 self.version
156 }
157}
158
159#[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 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 pub fn unset(path: impl AsRef<str>) -> Self {
183 Self::Unset {
184 path: path.as_ref().to_owned(),
185 }
186 }
187
188 pub fn remove(path: impl AsRef<str>) -> Self {
190 Self::Remove {
191 path: path.as_ref().to_owned(),
192 }
193 }
194
195 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 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 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 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 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 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#[derive(Clone, Debug, Default, Eq, PartialEq)]
252pub struct PatchSet {
253 operations: Vec<PatchOperation>,
254}
255
256impl PatchSet {
257 pub fn new(operations: Vec<PatchOperation>) -> Self {
259 Self { operations }
260 }
261
262 pub fn operations(&self) -> &[PatchOperation] {
264 &self.operations
265 }
266
267 pub fn with_operation(mut self, operation: PatchOperation) -> Self {
269 self.operations.push(operation);
270 self
271 }
272}
273
274#[derive(Clone, Debug, Eq, PartialEq)]
276pub struct DocumentPatch {
277 document_id: DocumentId,
278 patch_set: PatchSet,
279}
280
281impl DocumentPatch {
282 pub fn new(document_id: DocumentId, patch_set: PatchSet) -> Self {
284 Self {
285 document_id,
286 patch_set,
287 }
288 }
289
290 pub const fn document_id(&self) -> &DocumentId {
292 &self.document_id
293 }
294
295 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}