add derive + docs
This commit is contained in:
parent
5da65fb603
commit
ca2b0036f0
11 changed files with 400 additions and 43 deletions
17
Cargo.lock
generated
17
Cargo.lock
generated
|
@ -164,6 +164,12 @@ version = "1.6.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952"
|
||||
|
||||
[[package]]
|
||||
name = "case"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd6c0e7b807d60291f42f33f58480c0bfafe28ed08286446f45e463728cf9c1c"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.5"
|
||||
|
@ -695,6 +701,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"chrono",
|
||||
"futures",
|
||||
"mongod_derive",
|
||||
"mongodb",
|
||||
"regex",
|
||||
"serde",
|
||||
|
@ -703,6 +710,16 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mongod_derive"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mongodb"
|
||||
version = "2.8.2"
|
||||
|
|
|
@ -12,3 +12,4 @@ 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"] }
|
||||
mongod_derive = { path = "./mongod_derive" }
|
95
README.md
95
README.md
|
@ -1,2 +1,97 @@
|
|||
# mongod
|
||||
mongod is a rust crate for a model based database on top of MongoDB.
|
||||
|
||||
## Usage
|
||||
`mongod` allows you to use structs as `models` with data. it is build upon a MongoDB database. You need to pass a Connection URI for the database to use via the `$DB_URI` environment variable.
|
||||
|
||||
### Models
|
||||
You can derive the `Model` and `Referencable` traits for your struct. This will provide you with functions like `insert()`, `delete()`, `update()`, `get()`, etc for your struct. Additionally you have to manually implement the `Validate` trait which ensures a consistent valid state of your struct in order to never insert invalid data into the database.
|
||||
|
||||
```rust
|
||||
use serde::{Deserialize, Serialize};
|
||||
use mongod::Historic;
|
||||
use mongod::Validate;
|
||||
use mongod::Reference;
|
||||
use mongod::derive::{Module, Referencable};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Module, Referencable)]
|
||||
struct MyStruct {
|
||||
_id: String,
|
||||
name: String,
|
||||
other: Option<Reference>,
|
||||
}
|
||||
|
||||
impl Validate for MyStruct {
|
||||
async fn validate(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This allows you to use your struct like this:
|
||||
|
||||
```rust
|
||||
let a = MyStruct{
|
||||
_id: "someid".to_string(),
|
||||
name: "hello".to_string(),
|
||||
other: None
|
||||
};
|
||||
|
||||
a.insert().await;
|
||||
a.update(serde_json::json!({"name": "bye"})).await.unwrap();
|
||||
|
||||
let new_a = MyStruct::get("someid");
|
||||
```
|
||||
|
||||
### Historic data
|
||||
If you want certain fields to remember a history of changes you can use the `Historic<T>` type.
|
||||
This type will retain previous changes along with a timestamp when updates happen.
|
||||
|
||||
### References
|
||||
A Models field can be a reference to another model using the `Reference` type.
|
||||
|
||||
```rust
|
||||
let m = MyStruct::get("someid").await.unwrap();
|
||||
|
||||
// get a reference from model
|
||||
// `reference()` is a convenience function and is exactly the same as the two references below
|
||||
let myref = m.reference();
|
||||
let sec_ref = Reference::new(MyStruct::collection_name(), "someid").await.unwrap();
|
||||
let third_ref = Reference::new("my_struct", "someid").await.unwrap();
|
||||
|
||||
|
||||
struct OtherStruct {
|
||||
link: Reference
|
||||
}
|
||||
|
||||
// store a reference
|
||||
let other = OtherStruct{
|
||||
link: myref
|
||||
}
|
||||
|
||||
let m: MyStruct = other.link.get().await; // get back a model from reference
|
||||
```
|
||||
|
||||
With the `assert_reference_of!()` macro you can limit the models your reference can represent at validation. You can specify as many models as you want, but the reference must match one of them.
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Module, Referencable)]
|
||||
struct OtherStruct {
|
||||
/* *** */
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Module, Referencable)]
|
||||
struct YetAnotherStruct {
|
||||
/* *** */
|
||||
}
|
||||
|
||||
impl Validate for MyStruct {
|
||||
async fn validate(&self) -> bool {
|
||||
if let Some(other) = self.other {
|
||||
assert_reference_of!(other, OtherStruct, YetAnotherStruct);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
```
|
54
mongod_derive/Cargo.lock
generated
Normal file
54
mongod_derive/Cargo.lock
generated
Normal file
|
@ -0,0 +1,54 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "case"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd6c0e7b807d60291f42f33f58480c0bfafe28ed08286446f45e463728cf9c1c"
|
||||
|
||||
[[package]]
|
||||
name = "mongod_derive"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
13
mongod_derive/Cargo.toml
Normal file
13
mongod_derive/Cargo.toml
Normal file
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "mongod_derive"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "1.0", features = ["full"] }
|
||||
quote = "1.0"
|
||||
proc-macro2 = "1.0"
|
||||
case = "1.0.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
135
mongod_derive/src/lib.rs
Normal file
135
mongod_derive/src/lib.rs
Normal file
|
@ -0,0 +1,135 @@
|
|||
extern crate proc_macro;
|
||||
use case::CaseExt;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, Data, DeriveInput, Fields, Type, TypePath};
|
||||
|
||||
/// Get inner type. Example: Returns `T` for `Option<T>`.
|
||||
fn extract_inner_type<'a>(ty: &'a Type, parent: &'a str) -> Option<&'a Type> {
|
||||
if let Type::Path(type_path) = ty {
|
||||
if type_path.path.segments.len() == 1 {
|
||||
let segment = &type_path.path.segments[0];
|
||||
if segment.ident == parent {
|
||||
if let syn::PathArguments::AngleBracketed(ref args) = segment.arguments {
|
||||
if args.args.len() == 1 {
|
||||
if let syn::GenericArgument::Type(ref inner_type) = args.args[0] {
|
||||
return Some(inner_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn type_path(ty: &syn::Type) -> TypePath {
|
||||
if let syn::Type::Path(type_path) = ty {
|
||||
return type_path.clone();
|
||||
}
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
fn is_type(ty: &syn::Type, t: &str) -> bool {
|
||||
let type_path = type_path(ty);
|
||||
let id = type_path.path.segments.first().unwrap().ident.to_string();
|
||||
id == t
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Model)]
|
||||
pub fn model_derive(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
let name = input.ident;
|
||||
|
||||
// Generate code for each field
|
||||
let field_code = if let Data::Struct(data_struct) = input.data {
|
||||
match data_struct.fields {
|
||||
Fields::Named(fields_named) => {
|
||||
let field_process_code: Vec<_> = fields_named.named.iter().map(|field| {
|
||||
let field_name = &field.ident.as_ref().unwrap();
|
||||
let field_type = &field.ty;
|
||||
let field_name_str = field_name.to_string();
|
||||
|
||||
if field_name_str == "_id" {
|
||||
return quote! {};
|
||||
}
|
||||
|
||||
if is_type(field_type, "Historic") {
|
||||
let inner_field_type = extract_inner_type(field_type, "Historic").unwrap();
|
||||
if is_type(inner_field_type, "Vec") {
|
||||
return quote! {
|
||||
mongod::update_historic_vec!(self, obj, #field_name_str, #field_name, update);
|
||||
};
|
||||
}
|
||||
|
||||
return quote! {
|
||||
mongod::update_historic_str!(self, obj, #field_name_str, #field_name, update);
|
||||
}
|
||||
}
|
||||
|
||||
if is_type(field_type, "Option") {
|
||||
let inner_field_type = extract_inner_type(field_type, "Option").unwrap();
|
||||
|
||||
if is_type(inner_field_type, "Historic") {
|
||||
let sub_inner_field_type = extract_inner_type(inner_field_type, "Historic").unwrap();
|
||||
if is_type(sub_inner_field_type, "Reference") {
|
||||
return quote! {
|
||||
mongod::update_historic_ref_option!(self, obj, #field_name_str, #field_name, update);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return quote! {
|
||||
mongod::update_value_option!(self, obj, #field_name_str, #field_name, update, #inner_field_type);
|
||||
};
|
||||
}
|
||||
|
||||
quote! {
|
||||
mongod::update_value!(self, obj, #field_name_str, #field_name, update, #field_type);
|
||||
}
|
||||
}).collect();
|
||||
|
||||
quote! {
|
||||
impl mongod::model::Model for #name {
|
||||
async fn update_values(
|
||||
&mut self,
|
||||
obj: &serde_json::Map<String, serde_json::Value>,
|
||||
update: &mut mongodb::bson::Document,
|
||||
) {
|
||||
#( #field_process_code )*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
} else {
|
||||
unimplemented!()
|
||||
};
|
||||
|
||||
TokenStream::from(field_code)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Referencable)]
|
||||
pub fn referencable_derive(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
let name = input.ident;
|
||||
let name_str = name.to_string().to_snake();
|
||||
|
||||
let code = quote! {
|
||||
impl mongod::model::reference::Referencable for MyStruct {
|
||||
fn collection_name() -> &'static str {
|
||||
#name_str
|
||||
}
|
||||
|
||||
fn id(&self) -> &str {
|
||||
&self._id
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(code)
|
||||
}
|
|
@ -1,4 +1,9 @@
|
|||
pub mod model;
|
||||
pub use model::historic::Historic;
|
||||
pub use model::reference::*;
|
||||
pub use model::valid::Validate;
|
||||
pub use model::Model;
|
||||
pub use mongod_derive as derive;
|
||||
|
||||
/// Get a `MongoDB` Client from the environment
|
||||
#[macro_export]
|
||||
|
|
109
src/model/mod.rs
109
src/model/mod.rs
|
@ -23,52 +23,83 @@ pub enum UpdateError {
|
|||
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();
|
||||
/// Insert the `Model` into the database
|
||||
fn insert(&self) -> impl std::future::Future<Output = ()> + Send
|
||||
where
|
||||
Self: Sync,
|
||||
{
|
||||
async {
|
||||
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()
|
||||
/// Remove a `Model` from the database.
|
||||
fn remove(id: &str) -> impl std::future::Future<Output = ()> + Send {
|
||||
async move {
|
||||
let db = get_mongo!();
|
||||
let collection = col!(db, Self::collection_name());
|
||||
collection.delete_one(id_of!(id), None).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
/// Remove a `Model` from the database.
|
||||
///
|
||||
/// This is a convenience function to let you call `remove()` on a `Model` you have at hand.
|
||||
fn delete(&self) -> impl std::future::Future<Output = ()> + Send
|
||||
where
|
||||
Self: Sync,
|
||||
{
|
||||
async { Self::remove(self.id()).await }
|
||||
}
|
||||
|
||||
if let Some(obj) = data.as_object() {
|
||||
// run model specific update
|
||||
self.update_values(obj, &mut update).await;
|
||||
/// Get a `Model` by id from the database
|
||||
#[must_use]
|
||||
fn get(id: &str) -> impl std::future::Future<Output = Option<Self>> {
|
||||
async move {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
/// Update values of `Model` into database
|
||||
fn update(
|
||||
&mut self,
|
||||
data: &serde_json::Value,
|
||||
) -> impl std::future::Future<Output = Result<(), UpdateError>> {
|
||||
async {
|
||||
// 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(());
|
||||
}
|
||||
return Err(UpdateError::Validation);
|
||||
}
|
||||
}
|
||||
|
||||
Err(UpdateError::NoObject)
|
||||
Err(UpdateError::NoObject)
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the `Model` based on the provided JSON Object.
|
||||
|
@ -77,9 +108,9 @@ pub trait Model:
|
|||
/// 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(
|
||||
fn update_values(
|
||||
&mut self,
|
||||
obj: &Map<String, Value>,
|
||||
update: &mut mongodb::bson::Document,
|
||||
);
|
||||
) -> impl std::future::Future<Output = ()> + Send;
|
||||
}
|
||||
|
|
|
@ -9,8 +9,8 @@ use super::valid::Validate;
|
|||
pub struct Reference(String);
|
||||
|
||||
impl Reference {
|
||||
/// Create a new reference
|
||||
pub async fn new(reference: &str) -> Option<Self> {
|
||||
/// Create a new reference from String
|
||||
pub async fn new_raw(reference: &str) -> Option<Self> {
|
||||
let r = Self(reference.to_string());
|
||||
if r.validate().await {
|
||||
Some(r)
|
||||
|
@ -19,7 +19,13 @@ impl Reference {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create a new reference
|
||||
pub async fn new(model: &str, id: &str) -> Option<Self> {
|
||||
Self::new_raw(&format!("{model}::{id}")).await
|
||||
}
|
||||
|
||||
/// Checks if a reference is part of a model collection.
|
||||
#[must_use]
|
||||
pub fn is_of_collection(&self, col: &str) -> bool {
|
||||
self.0.starts_with(col)
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ 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 value = Historic::new(Reference::new_raw(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());
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/// This trait allows a `Model` to be validated.
|
||||
pub trait Validate {
|
||||
/// Validate the `Model`
|
||||
async fn validate(&self) -> bool;
|
||||
fn validate(&self) -> impl std::future::Future<Output = bool> + Send;
|
||||
}
|
||||
|
||||
/// Validate a value and return `false` if validation fails.
|
||||
|
|
Loading…
Reference in a new issue