This commit is contained in:
JMARyA 2024-07-17 09:40:59 +02:00
commit 5da65fb603
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
10 changed files with 2232 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1799
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

14
Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "mongod"
version = "0.1.0"
edition = "2021"
[dependencies]
chrono = "0.4.38"
futures = "0.3.30"
mongodb = "2.8.0"
regex = "1.10.5"
serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.111"
tokio = { version = "1.35.1", features = ["full"] }
uuid = { version = "1.8.0", features = ["v4"] }

2
README.md Normal file
View file

@ -0,0 +1,2 @@
# mongod
mongod is a rust crate for a model based database on top of MongoDB.

43
src/lib.rs Normal file
View file

@ -0,0 +1,43 @@
pub mod model;
/// Get a `MongoDB` Client from the environment
#[macro_export]
macro_rules! get_mongo {
() => {
mongodb::Client::with_uri_str(std::env::var("DB_URI").unwrap())
.await
.unwrap()
};
}
/// Get a database collection
#[macro_export]
macro_rules! col {
($db:expr, $col:expr) => {
$db.database("owl")
.collection::<mongodb::bson::Document>($col)
};
}
/// Collect database results into a `Vec<_>`
#[macro_export]
macro_rules! collect_results {
($res:expr) => {{
use futures::stream::TryStreamExt;
let mut ret = vec![];
while let Some(doc) = $res.try_next().await.unwrap() {
ret.push(doc);
}
ret
}};
}
/// `MongoDB` filter for the `_id` field.
#[macro_export]
macro_rules! id_of {
($id:expr) => {
mongodb::bson::doc! { "_id": $id}
};
}

44
src/model/historic.rs Normal file
View file

@ -0,0 +1,44 @@
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, ops::Deref};
/// A struct to keep track of historical changes to a value.
/// This struct represents a value that has a current state and a history of previous states.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Historic<T> {
/// The current value
pub current: T,
/// A map of timestamps (RFC 3339) to historical values.
pub changes: HashMap<String, T>,
}
impl<T: Clone + std::cmp::PartialEq> Historic<T> {
/// Create a new value with tracked history
pub fn new(value: T) -> Historic<T> {
let mut changes = HashMap::new();
changes.insert(chrono::Utc::now().to_rfc3339(), value.clone());
Self {
current: value,
changes,
}
}
/// Update the value. The change will be recorded.
pub fn update(&mut self, value: T) {
if self.current == value {
return;
}
self.changes
.insert(chrono::Utc::now().to_rfc3339(), value.clone());
self.current = value;
}
}
impl<T> Deref for Historic<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.current
}
}

85
src/model/mod.rs Normal file
View file

@ -0,0 +1,85 @@
use reference::Referencable;
use serde_json::{Map, Value};
use valid::Validate;
use crate::{col, get_mongo, id_of};
pub mod historic;
pub mod reference;
pub mod update;
pub mod valid;
/// Error type when updating a model
#[derive(Debug)]
pub enum UpdateError {
/// Provided data was no object
NoObject,
/// Database related error
Database,
/// Validation failed
Validation,
}
pub trait Model:
Sized + Referencable + Validate + serde::Serialize + for<'a> serde::Deserialize<'a>
{
/// Insert the model into the database
async fn insert(&self) {
let db = get_mongo!();
let collection = col!(db, Self::collection_name());
collection
.insert_one(mongodb::bson::to_document(self).unwrap(), None)
.await
.unwrap();
}
/// Get a model by id from the database
async fn get(id: &str) -> Option<Self> {
let db = get_mongo!();
let collection = col!(db, Self::collection_name());
let doc = collection.find_one(id_of!(id), None).await.ok()??;
mongodb::bson::from_document(doc).ok()
}
/// Update values of model into database
async fn update(&mut self, data: &serde_json::Value) -> Result<(), UpdateError> {
// get db collection
let db = get_mongo!();
let collection = col!(db, Self::collection_name());
let mut update = mongodb::bson::Document::new();
if let Some(obj) = data.as_object() {
// run model specific update
self.update_values(obj, &mut update).await;
// validate and update
if self.validate().await {
collection
.update_one(
id_of!(self.id()),
mongodb::bson::doc! {"$set": update },
None,
)
.await
.map_err(|_| UpdateError::Database)?;
return Ok(());
} else {
return Err(UpdateError::Validation);
}
}
Err(UpdateError::NoObject)
}
/// Update the `Model` based on the provided JSON Object.
///
/// This function should be implemented for a `Model`. It should update all keys from the JSON Object.
/// For every updated value in the JSON Object the `Model`s fields should be updated and the value should be put in the `update` `Document`
///
/// To avoid making mistakes in the Update logic use the macros from `update.rs`
async fn update_values(
&mut self,
obj: &Map<String, Value>,
update: &mut mongodb::bson::Document,
);
}

62
src/model/reference.rs Normal file
View file

@ -0,0 +1,62 @@
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use crate::{col, get_mongo, id_of};
use super::valid::Validate;
/// A `Reference` to a `Model`
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct Reference(String);
impl Reference {
/// Create a new reference
pub async fn new(reference: &str) -> Option<Self> {
let r = Self(reference.to_string());
if r.validate().await {
Some(r)
} else {
None
}
}
/// Checks if a reference is part of a model collection.
pub fn is_of_collection(&self, col: &str) -> bool {
self.0.starts_with(col)
}
/// Get the raw `Document` behind the `Reference` from the database.
pub async fn get_document(&self) -> Option<mongodb::bson::Document> {
let (col, id) = self.0.split_once("::")?;
let db = get_mongo!();
let col = col!(db, col);
col.find_one(id_of!(id), None).await.ok()?
}
/// Get the `Model` behind the `Reference` from the database.
pub async fn get<T: DeserializeOwned>(&self) -> T {
mongodb::bson::from_document(self.get_document().await.unwrap()).unwrap()
}
}
impl Validate for Reference {
async fn validate(&self) -> bool {
// cheap
//self.0.split_once("::").is_some()
// right
self.get_document().await.is_some()
}
}
/// A trait that allows you to get a reference to a `Model`
pub trait Referencable {
/// The name of this model's collection
fn collection_name() -> &'static str;
/// The id of this model
fn id(&self) -> &str;
/// Get a reference to the object
fn reference(&self) -> Reference {
Reference(format!("{}::{}", Self::collection_name(), self.id()))
}
}

142
src/model/update.rs Normal file
View file

@ -0,0 +1,142 @@
/// Updates the value of an entity off JSON data.
///
/// This macro updates the given field of the entity with the parsed value from the JSON object.
/// If the key is present in the JSON, it will be updated. Otherwise, it will not be updated.
///
/// # Parameters
///
/// * `$entity`: The entity that contains the field to be updated.
/// * `$json`: The JSON object containing the update values to be parsed.
/// * `$key`: The literal key to search for in the JSON object.
/// * `$field`: The field name of the entity that should be updated.
/// * `$update`: A BSON Document for updating the database
#[macro_export]
macro_rules! update_historic_str {
($entity:ident, $json:ident, $key:literal, $field:ident, $update:ident) => {
if let Some(val) = $json.get($key) {
if let Some(val_str) = val.as_str() {
let mut field = $entity.$field.clone();
if !(&field.current == val_str) {
field.update(val_str.to_owned());
$entity.$field = field;
$update.insert($key, mongodb::bson::to_bson(&$entity.$field).unwrap());
}
}
}
};
}
/// Updates the value of an entity off JSON data.
///
/// This macro updates the given field of the entity with the parsed value from the JSON object.
/// If the key is present in the JSON, it will be updated. Otherwise, it will not be updated.
///
/// # Parameters
///
/// * `$entity`: The entity that contains the field to be updated.
/// * `$json`: The JSON object containing the update values to be parsed.
/// * `$key`: The literal key to search for in the JSON object.
/// * `$field`: The field name of the entity that should be updated.
/// * `$update`: A BSON Document for updating the database
#[macro_export]
macro_rules! update_historic_ref_option {
($entity:ident, $json:ident, $key:literal, $field:ident, $update:ident) => {
if let Some(val) = $json.get($key) {
if let Some(val_str) = val.as_str() {
let value = Historic::new(Reference::new(val_str).await.unwrap());
let field = $entity.$field.clone().unwrap_or_else(|| value);
$entity.$field = Some(field);
$update.insert($key, mongodb::bson::to_bson(&$entity.$field).unwrap());
}
}
};
}
/// Updates the value of an entity off JSON data.
///
/// This macro updates the given field of the entity with the parsed value from the JSON object.
/// If the key is present in the JSON, it will be updated. Otherwise, it will not be updated.
///
/// # Parameters
///
/// * `$entity`: The entity that contains the field to be updated.
/// * `$json`: The JSON object containing the update values to be parsed.
/// * `$key`: The literal key to search for in the JSON object.
/// * `$field`: The field name of the entity that should be updated.
/// * `$update`: A BSON Document for updating the database
#[macro_export]
macro_rules! update_historic_vec {
($entity:ident, $json:ident, $key:literal, $field:ident, $update:ident) => {
if let Some(val) = $json.get($key) {
if let Some(val) = val.as_array() {
let mut field = $entity.$field.clone();
field.update(
val.into_iter()
.map(|x| serde_json::from_value(x.clone()).unwrap())
.collect(),
);
$entity.$field = field;
$update.insert($key, mongodb::bson::to_bson(&$entity.$field).unwrap());
}
}
};
}
/// Updates the value of an entity off JSON data.
///
/// This macro updates the given field of the entity with the parsed value from the JSON object.
/// If the key is present in the JSON, it will be updated. Otherwise, it will not be updated.
///
/// # Parameters
///
/// * `$entity`: The entity that contains the field to be updated.
/// * `$json`: The JSON object containing the update values to be parsed.
/// * `$key`: The literal key to search for in the JSON object.
/// * `$field`: The field name of the entity that should be updated.
/// * `$update`: A BSON Document for updating the database
/// * `$t`: The type of the value
#[macro_export]
macro_rules! update_value {
($entity:ident, $json:ident, $key:literal, $field:ident, $update:ident, $t:ty) => {
if let Some(val) = $json.get($key) {
if let Ok(val) = serde_json::from_value::<$t>(val.clone()) {
if val != $entity.$field {
$entity.$field = val;
$update.insert($key, mongodb::bson::to_bson(&$entity.$field).unwrap());
}
}
}
};
}
/// Updates the value of an entity off JSON data.
///
/// This macro updates the given field of the entity with the parsed value from the JSON object.
/// If the key is present in the JSON, it will be updated. Otherwise, it will not be updated.
///
/// # Parameters
///
/// * `$entity`: The entity that contains the field to be updated.
/// * `$json`: The JSON object containing the update values to be parsed.
/// * `$key`: The literal key to search for in the JSON object.
/// * `$field`: The field name of the entity that should be updated.
/// * `$update`: A BSON Document for updating the database
/// * `$t`: The type of the value
#[macro_export]
macro_rules! update_value_option {
($entity:ident, $json:ident, $key:literal, $field:ident, $update:ident, $t:ty) => {
if let Some(val) = $json.get($key) {
if let Ok(val) = serde_json::from_value::<$t>(val.clone()) {
if let Some(v) = &$entity.$field {
if val != *v {
$entity.$field = Some(val);
$update.insert($key, mongodb::bson::to_bson(&$entity.$field).unwrap());
}
} else {
$entity.$field = Some(val);
$update.insert($key, mongodb::bson::to_bson(&$entity.$field).unwrap());
}
}
}
};
}

40
src/model/valid.rs Normal file
View file

@ -0,0 +1,40 @@
/// This trait allows a `Model` to be validated.
pub trait Validate {
/// Validate the `Model`
async fn validate(&self) -> bool;
}
/// Validate a value and return `false` if validation fails.
#[macro_export]
macro_rules! validate {
($val:expr) => {
if !$val.validate().await {
return false;
}
};
}
/// This macro checks for the type of a reference and is useful for validation.
/// It will check all supplied types and return `false` if none are matching.
///
/// # Example
/// ```
/// fn validate(&self) -> bool {
/// assert_reference_of!(self.owner, Person);
/// true
/// }
/// ```
#[macro_export]
macro_rules! assert_reference_of {
($var:expr, $($struct_name:ident),+) => {
let mut match_found = false;
$(
if $var.is_of_collection($struct_name::collection_name()) {
match_found = true;
}
)*
if !match_found {
return false;
}
};
}