builder update

This commit is contained in:
JMARyA 2024-09-11 11:09:38 +02:00
parent 9784c3cac6
commit d830e677ba
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
8 changed files with 461 additions and 99 deletions

View file

@ -105,3 +105,34 @@ let ms = MyStruct::get_partial("someid", &serde_json::json!({"other": 1})).await
let myref = ms.other.unwrap(); // will be there let myref = ms.other.unwrap(); // will be there
let name = ms.name.unwrap() // will panic! let name = ms.name.unwrap() // will panic!
``` ```
### Updating values
You can either update the values by passing a JSON object overwriting the current values or update the values using a builder pattern.
```rust
[...]
struct MyStruct {
_id: String,
name: String,
age: u32,
other: Option<Reference>,
}
let mut a = MyStruct::get("someid").await.unwrap();
// JSON
a.update(serde_json::json!({"name": "bye"})).await.unwrap();
// Builder
let mut changes = a.change();
// Set fields
changes = changes.name("bye");
// There are type specific functions
// Increment age by 1
changes = changes.age_increment(1);
// Finalize
let changed_model: MyStruct = changes.update().await.unwrap();
```

View file

@ -0,0 +1,213 @@
use quote::quote;
use syn::Field;
use crate::{extract_inner_type, is_one_of_type, is_type};
/// Generate the ChangeBuilder field fns
pub fn builder_change_fields(field: &Field) -> proc_macro2::TokenStream {
let field_name = &field.ident.as_ref().unwrap();
let field_type = &field.ty;
let field_name_str = field_name.to_string();
// Never update _id
if field_name_str == "_id" {
return quote! {};
}
let number_types = [
"u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128", "isize",
"f16", "f32", "f64", "f128",
];
// Number type fn
if is_one_of_type(field_type, &number_types) {
let documentation = format!("Set the value of `{field_name}`");
let inc_fn_name = syn::Ident::new(&format!("{}_increment", field_name), field_name.span());
let doc_inc = format!("Increment value of `{field_name}` by `value`. Consecutive calls to this function will not add up, they overwrite the increment.");
let mul_fn_name = syn::Ident::new(&format!("{}_multiply", field_name), field_name.span());
let doc_mul = format!("Multiply value of `{field_name}` by `value`. Consecutive calls to this function will not add up, they overwrite the multiply.");
return quote! {
#[doc = #documentation]
pub fn #field_name(mut self, value: #field_type)-> Self {
self.model.#field_name = value.into();
self.changeset.entry("$set".to_string()).or_insert(mongod::mongodb::bson::doc! {}.into()).as_document_mut().unwrap().insert(
#field_name_str.to_string(),
mongod::mongodb::bson::to_bson(&self.model.#field_name).unwrap(),
);
self
}
#[doc = #doc_inc]
pub fn #inc_fn_name(mut self, value: #field_type) -> Self {
self.model.#field_name += value;
self.changeset.entry("$inc".to_string()).or_insert(mongod::mongodb::bson::doc! {}.into()).as_document_mut().unwrap()
.insert(
#field_name_str.to_string(),
mongod::mongodb::bson::to_bson(&value).unwrap(),
);
self
}
#[doc = #doc_mul]
pub fn #mul_fn_name(mut self, value: #field_type) -> Self {
self.model.#field_name *= value;
self.changeset.entry("$mul".to_string()).or_insert(mongod::mongodb::bson::doc! {}.into()).as_document_mut().unwrap()
.insert(
#field_name_str.to_string(),
mongod::mongodb::bson::to_bson(&value).unwrap(),
);
self
}
};
}
if is_type(field_type, "Vec") {
let inner_field_type = extract_inner_type(field_type, "Vec").unwrap();
let push_fn_name = syn::Ident::new(&format!("{}_push", field_name), field_name.span());
let documentation = format!("Add a value to the Vec `{field_name}`");
let documentation2 = format!("Set the value of `{field_name}`");
return quote! {
#[doc = #documentation]
pub fn #push_fn_name<T>(mut self, value: T) -> Self where T: Into<#inner_field_type> + serde::Serialize {
let mut push = self.changeset.entry("$push".to_string()).or_insert(mongod::mongodb::bson::doc! {}.into()).as_document_mut().unwrap();
if push.contains_key(#field_name_str) {
let current = push.get_mut(#field_name_str.to_string()).unwrap();
if current.as_document().map(|x| !x.contains_key("$each")).unwrap_or(true) {
let each = mongod::mongodb::bson::doc! {
"$each": [current, mongod::mongodb::bson::to_bson(&value).unwrap()]
};
push.insert(#field_name_str.to_string(), each);
} else {
current.as_document_mut().unwrap().get_mut("$each").unwrap().as_array_mut().unwrap().push(mongod::mongodb::bson::to_bson(&value).unwrap());
}
} else {
push.insert(#field_name_str.to_string(), mongod::mongodb::bson::to_bson(&value).unwrap());
}
self.model.#field_name.push(value.into());
self
}
#[doc = #documentation2]
pub fn #field_name<T>(mut self, value: T)-> Self where T: Into<#field_type> + serde::Serialize {
self.model.#field_name = value.into();
self.changeset.entry("$set".to_string()).or_insert(mongod::mongodb::bson::doc! {}.into()).as_document_mut().unwrap().insert(
#field_name_str.to_string(),
mongod::mongodb::bson::to_bson(&self.model.#field_name).unwrap(),
);
self
}
};
}
if is_type(field_type, "Historic") {
let inner_field_type = extract_inner_type(field_type, "Historic").unwrap();
let documentation = format!(
"Update the value of `{field_name}`. This change will be recorded by the `Historic`"
);
// Code for Historic<T>
return quote! {
#[doc = #documentation]
pub fn #field_name<T>(mut self, value: T) -> Self where T: Into<#inner_field_type> + serde::Serialize {
self.model.#field_name.update(value.into());
self.changeset.entry("$set".to_string()).or_insert(mongod::mongodb::bson::doc! {}.into()).as_document_mut().unwrap().insert(
#field_name_str.to_string(),
mongod::mongodb::bson::to_bson(&self.model.#field_name).unwrap(),
);
self
}
};
}
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 inner_field_type = extract_inner_type(inner_field_type, "Historic").unwrap();
let documentation = format!("Update the value of `{field_name}`. This change will be recorded by the `Historic`. If `{field_name}` is `None` a new `Historic` will be initialized.");
// Code for Option<Historic<T>>
return quote! {
#[doc = #documentation]
pub fn #field_name<T>(mut self, value: T) -> Self where T: Into<#inner_field_type> + serde::Serialize {
if let Some(mut opt) = self.model.#field_name.as_mut() {
opt.update(value.into());
} else {
self.model.#field_name = Some(mongod::Historic::new(value.into()));
}
self.changeset.entry("$set".to_string()).or_insert(mongod::mongodb::bson::doc! {}.into()).as_document_mut().unwrap().insert(
#field_name_str.to_string(),
mongod::mongodb::bson::to_bson(&self.model.#field_name).unwrap(),
);
self
}
};
}
let documentation = format!("Set the value of `{field_name}`. If `Some(_)` it will be updated or removed if it is `None`");
return quote! {
#[doc = #documentation]
pub fn #field_name(mut self, value: Option<#inner_field_type>) -> Self {
let is_some = value.is_some();
self.model.#field_name = value.into();
if is_some {
self.changeset.entry("$set".to_string()).or_insert(mongod::mongodb::bson::doc! {}.into()).as_document_mut().unwrap().insert(
#field_name_str.to_string(),
mongod::mongodb::bson::to_bson(&self.model.#field_name).unwrap(),
);
} else {
self.changeset.entry("$unset".to_string()).or_insert(mongod::mongodb::bson::doc! {}.into()).as_document_mut().unwrap().insert(
#field_name_str.to_string(),
mongod::mongodb::bson::to_bson("").unwrap(),
);
}
self
}
};
}
let documentation = format!("Set the value of `{field_name}`");
// Code for T
quote! {
#[doc = #documentation]
pub fn #field_name<T>(mut self, value: T)-> Self where T: Into<#field_type> + serde::Serialize {
self.model.#field_name = value.into();
self.changeset.entry("$set".to_string()).or_insert(mongod::mongodb::bson::doc! {}.into()).as_document_mut().unwrap().insert(
#field_name_str.to_string(),
mongod::mongodb::bson::to_bson(&self.model.#field_name).unwrap(),
);
self
}
}
}

View file

@ -3,40 +3,18 @@ use case::CaseExt;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields, Type, TypePath}; use syn::{parse_macro_input, Data, DeriveInput, Fields};
/// Get inner type. Example: Returns `T` for `Option<T>`. mod types;
fn extract_inner_type<'a>(ty: &'a Type, parent: &'a str) -> Option<&'a Type> { use types::*;
if let Type::Path(type_path) = ty { mod partial_fields;
if type_path.path.segments.len() == 1 { use partial_fields::partial_code_field;
let segment = &type_path.path.segments[0]; mod update_fields;
if segment.ident == parent { use update_fields::update_code_field;
if let syn::PathArguments::AngleBracketed(ref args) = segment.arguments { mod change_fields;
if args.args.len() == 1 { use change_fields::builder_change_fields;
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
}
/// #[derive(Model)]
#[proc_macro_derive(Model)] #[proc_macro_derive(Model)]
pub fn model_derive(input: TokenStream) -> TokenStream { pub fn model_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput); let input = parse_macro_input!(input as DeriveInput);
@ -44,84 +22,37 @@ pub fn model_derive(input: TokenStream) -> TokenStream {
let name = input.ident; let name = input.ident;
let name_str = name.to_string().to_snake(); let name_str = name.to_string().to_snake();
let partial_name = syn::Ident::new(&format!("Partial{}", name), name.span()); let partial_name = syn::Ident::new(&format!("Partial{}", name), name.span());
let changebuilder_name = syn::Ident::new(&format!("Change{}", name), name.span());
let update_doc = "Commit the builder changes to DB";
// Generate code for each field // Generate code for each field
let field_code = if let Data::Struct(data_struct) = input.data { let field_code = if let Data::Struct(data_struct) = input.data {
match data_struct.fields { match data_struct.fields {
Fields::Named(fields_named) => { Fields::Named(fields_named) => {
let field_process_code: Vec<_> = fields_named.named.iter().map(|field| { // Update code
let field_name = &field.ident.as_ref().unwrap(); let field_process_code: Vec<_> =
let field_type = &field.ty; fields_named.named.iter().map(update_code_field).collect();
let field_name_str = field_name.to_string();
if field_name_str == "_id" { // Partial struct fields
return quote! {}; let partial_struct: Vec<_> =
} fields_named.named.iter().map(partial_code_field).collect();
if is_type(field_type, "Historic") { // Builder functions
let inner_field_type = extract_inner_type(field_type, "Historic").unwrap(); let builder_change_fields: Vec<_> = fields_named
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();
let partial_struct: Vec<_> = fields_named
.named .named
.iter() .iter()
.map(|field| { .map(builder_change_fields)
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! {
pub _id: String,
};
}
if is_type(field_type, "Option") {
return quote! {
pub #field_name: #field_type,
};
}
quote! {
pub #field_name: Option<#field_type>,
}
})
.collect(); .collect();
quote! { quote! {
impl mongod::model::Model for #name { impl mongod::model::Model for #name {
type Partial = #partial_name; type Partial = #partial_name;
type ChangeBuilder = #changebuilder_name;
fn change_builder(self) -> Self::ChangeBuilder {
#changebuilder_name::new(self)
}
async fn update_values( async fn update_values(
&mut self, &mut self,
@ -137,6 +68,47 @@ pub fn model_derive(input: TokenStream) -> TokenStream {
#( #partial_struct )* #( #partial_struct )*
} }
#[derive(Debug)]
pub struct #changebuilder_name {
model: #name,
changeset: mongod::mongodb::bson::Document
}
impl #changebuilder_name {
pub fn new(model: #name) -> Self {
Self {
model,
changeset: mongod::mongodb::bson::doc! {}
}
}
#[doc = #update_doc]
pub async fn update(self) -> Result<#name, mongod::model::UpdateError> {
let db = mongod::get_mongo!();
let collection = mongod::col!(db, <#name as mongod::Referencable>::collection_name());
if let Err(msg) = mongod::Validate::validate(&self.model).await {
return Err(mongod::model::UpdateError::Validation(msg));
}
let changeset = self.changeset;
collection
.update_one(
mongod::id_of!(mongod::Referencable::id(&self.model)),
changeset,
None,
)
.await
.map_err(mongod::model::UpdateError::Database)?;
Ok(self.model)
}
#( #builder_change_fields )*
}
impl mongod::model::reference::Referencable for #partial_name { impl mongod::model::reference::Referencable for #partial_name {
fn collection_name() -> &'static str { fn collection_name() -> &'static str {
#name_str #name_str
@ -157,6 +129,7 @@ pub fn model_derive(input: TokenStream) -> TokenStream {
TokenStream::from(field_code) TokenStream::from(field_code)
} }
/// #[derive(Referencable)]
#[proc_macro_derive(Referencable)] #[proc_macro_derive(Referencable)]
pub fn referencable_derive(input: TokenStream) -> TokenStream { pub fn referencable_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput); let input = parse_macro_input!(input as DeriveInput);

View file

@ -0,0 +1,30 @@
use quote::quote;
use syn::Field;
use crate::is_type;
/// Generate struct fields for Partial Model
pub fn partial_code_field(field: &Field) -> proc_macro2::TokenStream {
let field_name = &field.ident.as_ref().unwrap();
let field_type = &field.ty;
let field_name_str = field_name.to_string();
// Keep _id
if field_name_str == "_id" {
return quote! {
pub _id: String,
};
}
// Leave Option<T> alone
if is_type(field_type, "Option") {
return quote! {
pub #field_name: #field_type,
};
}
// Turn every field into Option<T>
quote! {
pub #field_name: Option<#field_type>,
}
}

View file

@ -0,0 +1,42 @@
use syn::{Type, TypePath};
/// Get inner type. Example: Returns `T` for `Option<T>`.
pub 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
}
pub fn type_path(ty: &syn::Type) -> TypePath {
if let syn::Type::Path(type_path) = ty {
return type_path.clone();
}
unreachable!();
}
pub fn is_one_of_type(ty: &syn::Type, t: &[&str]) -> bool {
for typ in t {
if is_type(ty, typ) {
return true;
}
}
false
}
pub 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
}

View file

@ -0,0 +1,55 @@
use quote::quote;
use syn::Field;
use crate::{extract_inner_type, is_type};
/// Generate code for the update fn of models
pub fn update_code_field(field: &Field) -> proc_macro2::TokenStream {
let field_name = &field.ident.as_ref().unwrap();
let field_type = &field.ty;
let field_name_str = field_name.to_string();
// Never update _id
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") {
// Custom code Historic<Vec<T>>
return quote! {
mongod::update_historic_vec!(self, obj, #field_name_str, #field_name, update);
};
}
// Code for Historic<T>
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") {
// Code for Option<Historic<Reference>>
return quote! {
mongod::update_historic_ref_option!(self, obj, #field_name_str, #field_name, update);
};
}
}
// Code for Option<T>
return quote! {
mongod::update_value_option!(self, obj, #field_name_str, #field_name, update, #inner_field_type);
};
}
// Code for T
quote! {
mongod::update_value!(self, obj, #field_name_str, #field_name, update, #field_type);
}
}

View file

@ -3,7 +3,7 @@ use std::{collections::HashMap, ops::Deref};
/// A struct to keep track of historical changes to a value. /// 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. /// This struct represents a value that has a current state and a history of previous states.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Historic<T> { pub struct Historic<T> {
/// The current value /// The current value
pub current: T, pub current: T,
@ -24,6 +24,13 @@ impl<T: Clone> Historic<T> {
} }
} }
impl<T: Default + Clone> Historic<T> {
/// Create a new tracked value initialized with Default
pub fn new_default() -> Historic<T> {
Self::new(T::default())
}
}
impl<T: Clone> Historic<T> { impl<T: Clone> Historic<T> {
/// Update the value. The change will be recorded. /// Update the value. The change will be recorded.
/// Will record a change even if the value is the same as the current one. /// Will record a change even if the value is the same as the current one.

View file

@ -18,8 +18,6 @@ pub mod reference;
pub mod update; pub mod update;
pub mod valid; pub mod valid;
// todo : use mongodb projection to only get fields you actually use, maybe PartialModel shadow struct?
/// Error type when updating a model /// Error type when updating a model
#[derive(Debug)] #[derive(Debug)]
pub enum UpdateError { pub enum UpdateError {
@ -35,6 +33,7 @@ pub trait Model:
Sized + Referencable + Validate + serde::Serialize + for<'a> serde::Deserialize<'a> Sized + Referencable + Validate + serde::Serialize + for<'a> serde::Deserialize<'a>
{ {
type Partial: DeserializeOwned; type Partial: DeserializeOwned;
type ChangeBuilder;
/// Insert the `Model` into the database /// Insert the `Model` into the database
fn insert( fn insert(
@ -293,6 +292,18 @@ pub trait Model:
} }
} }
fn change_builder(self) -> Self::ChangeBuilder;
/// Update values of `Model` using a builder pattern
fn change(self) -> Self::ChangeBuilder {
#[cfg(feature = "cache")]
{
mongod::cache_write!().invalidate(Self::collection_name(), self.id());
}
self.change_builder()
}
/// Update values of `Model` into database /// Update values of `Model` into database
fn update( fn update(
&mut self, &mut self,