add derive + docs

This commit is contained in:
JMARyA 2024-07-17 16:49:35 +02:00
parent 5da65fb603
commit ca2b0036f0
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
11 changed files with 400 additions and 43 deletions

17
Cargo.lock generated
View file

@ -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"

View file

@ -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" }

View file

@ -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
View 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
View 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
View 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)
}

View file

@ -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]

View file

@ -23,8 +23,12 @@ 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) {
/// 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
@ -32,17 +36,44 @@ pub trait Model:
.await
.unwrap();
}
}
/// Get a model by id from the database
async fn get(id: &str) -> Option<Self> {
/// 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();
}
}
/// 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 }
}
/// 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()
}
}
/// Update values of model into database
async fn update(&mut self, data: &serde_json::Value) -> Result<(), UpdateError> {
/// 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());
@ -63,13 +94,13 @@ pub trait Model:
.await
.map_err(|_| UpdateError::Database)?;
return Ok(());
} else {
return Err(UpdateError::Validation);
}
return Err(UpdateError::Validation);
}
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;
}

View file

@ -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)
}

View file

@ -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());

View file

@ -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.