init
This commit is contained in:
commit
5da65fb603
10 changed files with 2232 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
1799
Cargo.lock
generated
Normal file
1799
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal 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
2
README.md
Normal 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
43
src/lib.rs
Normal 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
44
src/model/historic.rs
Normal 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
85
src/model/mod.rs
Normal 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
62
src/model/reference.rs
Normal 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
142
src/model/update.rs
Normal 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
40
src/model/valid.rs
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
Loading…
Add table
Reference in a new issue