#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::fmt;
macro_rules! string_newtype {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct $name(String);
impl $name {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl From<String> for $name {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl From<&str> for $name {
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
};
}
string_newtype! {
CollectionName
}
string_newtype! {
DocumentId
}
string_newtype! {
DocumentField
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DocumentRevision(u64);
impl DocumentRevision {
pub const fn new(value: u64) -> Self {
Self(value)
}
pub const fn value(self) -> u64 {
self.0
}
}
impl fmt::Display for DocumentRevision {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.0)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DocumentVersion(u64);
impl DocumentVersion {
pub const fn new(value: u64) -> Self {
Self(value)
}
pub const fn value(self) -> u64 {
self.0
}
}
impl fmt::Display for DocumentVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.0)
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct DocumentMetadata {
collection: Option<CollectionName>,
revision: Option<DocumentRevision>,
version: Option<DocumentVersion>,
}
impl DocumentMetadata {
pub const fn new() -> Self {
Self {
collection: None,
revision: None,
version: None,
}
}
pub fn with_collection(mut self, collection: CollectionName) -> Self {
self.collection = Some(collection);
self
}
pub const fn with_revision(mut self, revision: DocumentRevision) -> Self {
self.revision = Some(revision);
self
}
pub const fn with_version(mut self, version: DocumentVersion) -> Self {
self.version = Some(version);
self
}
pub const fn collection(&self) -> Option<&CollectionName> {
self.collection.as_ref()
}
pub const fn revision(&self) -> Option<DocumentRevision> {
self.revision
}
pub const fn version(&self) -> Option<DocumentVersion> {
self.version
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum PatchOperation {
Set { path: String, value: String },
Unset { path: String },
Remove { path: String },
Increment { path: String, amount: i64 },
Append { path: String, value: String },
Prepend { path: String, value: String },
Merge { path: String, value: String },
Replace { path: String, value: String },
}
impl PatchOperation {
pub fn set(path: impl AsRef<str>, value: impl Into<String>) -> Self {
Self::Set {
path: path.as_ref().to_owned(),
value: value.into(),
}
}
pub fn unset(path: impl AsRef<str>) -> Self {
Self::Unset {
path: path.as_ref().to_owned(),
}
}
pub fn remove(path: impl AsRef<str>) -> Self {
Self::Remove {
path: path.as_ref().to_owned(),
}
}
pub fn increment(path: impl AsRef<str>, amount: i64) -> Self {
Self::Increment {
path: path.as_ref().to_owned(),
amount,
}
}
pub fn append(path: impl AsRef<str>, value: impl Into<String>) -> Self {
Self::Append {
path: path.as_ref().to_owned(),
value: value.into(),
}
}
pub fn prepend(path: impl AsRef<str>, value: impl Into<String>) -> Self {
Self::Prepend {
path: path.as_ref().to_owned(),
value: value.into(),
}
}
pub fn merge(path: impl AsRef<str>, value: impl Into<String>) -> Self {
Self::Merge {
path: path.as_ref().to_owned(),
value: value.into(),
}
}
pub fn replace(path: impl AsRef<str>, value: impl Into<String>) -> Self {
Self::Replace {
path: path.as_ref().to_owned(),
value: value.into(),
}
}
pub fn path(&self) -> &str {
match self {
Self::Set { path, .. }
| Self::Unset { path }
| Self::Remove { path }
| Self::Increment { path, .. }
| Self::Append { path, .. }
| Self::Prepend { path, .. }
| Self::Merge { path, .. }
| Self::Replace { path, .. } => path,
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct PatchSet {
operations: Vec<PatchOperation>,
}
impl PatchSet {
pub fn new(operations: Vec<PatchOperation>) -> Self {
Self { operations }
}
pub fn operations(&self) -> &[PatchOperation] {
&self.operations
}
pub fn with_operation(mut self, operation: PatchOperation) -> Self {
self.operations.push(operation);
self
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DocumentPatch {
document_id: DocumentId,
patch_set: PatchSet,
}
impl DocumentPatch {
pub fn new(document_id: DocumentId, patch_set: PatchSet) -> Self {
Self {
document_id,
patch_set,
}
}
pub const fn document_id(&self) -> &DocumentId {
&self.document_id
}
pub const fn patch_set(&self) -> &PatchSet {
&self.patch_set
}
}
#[cfg(test)]
mod tests {
use super::{
CollectionName, DocumentId, DocumentMetadata, DocumentPatch, DocumentRevision,
DocumentVersion, PatchOperation, PatchSet,
};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
#[test]
fn constructs_document_newtypes() {
let collection = CollectionName::new("customers");
let document_id = DocumentId::new("customer_123");
assert_eq!(collection.to_string(), "customers");
assert_eq!(document_id.as_ref(), "customer_123");
}
#[test]
fn hashes_equal_document_ids() {
let mut left = DefaultHasher::new();
let mut right = DefaultHasher::new();
DocumentId::new("same").hash(&mut left);
DocumentId::new("same").hash(&mut right);
assert_eq!(left.finish(), right.finish());
}
#[test]
fn builds_metadata_and_patch_operations() {
let metadata = DocumentMetadata::new()
.with_collection(CollectionName::new("customers"))
.with_revision(DocumentRevision::new(7))
.with_version(DocumentVersion::new(2));
let patch = PatchSet::new(vec![
PatchOperation::set("profile.display_name", "Joshua Whalen"),
PatchOperation::increment("stats.reviews", 1),
]);
let document_patch = DocumentPatch::new(DocumentId::new("customer_123"), patch);
assert_eq!(metadata.collection().unwrap().as_str(), "customers");
assert_eq!(metadata.revision(), Some(DocumentRevision::new(7)));
assert_eq!(
document_patch.patch_set().operations()[0].path(),
"profile.display_name"
);
}
}