Cont. "Enable convert from ActiveModel to Model" (#990)
* Changelog * Enable convert from ActiveModel to Model (#725) * feat: enable convert from ActiveModel to Model * feat: add tests for converting from ActiveModel to Model * cargo fmt * Refactoring Co-authored-by: Billy Chan <ccw.billy.123@gmail.com> * Fix clippy warnings * Use error type Co-authored-by: Chris Tsang <chris.2y3@outlook.com> Co-authored-by: greenhandatsjtu <40566803+greenhandatsjtu@users.noreply.github.com>
This commit is contained in:
parent
3f07bd19b1
commit
a766500ebf
@ -1,12 +1,19 @@
|
||||
use crate::util::{escape_rust_keyword, field_not_ignored, trim_starting_raw_identifier};
|
||||
use crate::util::{
|
||||
escape_rust_keyword, field_not_ignored, format_field_ident, trim_starting_raw_identifier,
|
||||
};
|
||||
use heck::CamelCase;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::{format_ident, quote, quote_spanned};
|
||||
use syn::{punctuated::Punctuated, token::Comma, Data, DataStruct, Field, Fields, Lit, Meta, Type};
|
||||
use syn::{
|
||||
punctuated::{IntoIter, Punctuated},
|
||||
token::Comma,
|
||||
Data, DataStruct, Field, Fields, Lit, Meta, Type,
|
||||
};
|
||||
|
||||
/// Method to derive an [ActiveModel](sea_orm::ActiveModel)
|
||||
pub fn expand_derive_active_model(ident: Ident, data: Data) -> syn::Result<TokenStream> {
|
||||
let fields = match data {
|
||||
// including ignored fields
|
||||
let all_fields = match data {
|
||||
Data::Struct(DataStruct {
|
||||
fields: Fields::Named(named),
|
||||
..
|
||||
@ -17,14 +24,21 @@ pub fn expand_derive_active_model(ident: Ident, data: Data) -> syn::Result<Token
|
||||
})
|
||||
}
|
||||
}
|
||||
.into_iter()
|
||||
.filter(field_not_ignored);
|
||||
.into_iter();
|
||||
|
||||
let field: Vec<Ident> = fields
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|Field { ident, .. }| format_ident!("{}", ident.unwrap().to_string()))
|
||||
.collect();
|
||||
let derive_active_model = derive_active_model(all_fields.clone())?;
|
||||
let derive_into_model = derive_into_model(all_fields)?;
|
||||
|
||||
Ok(quote!(
|
||||
#derive_active_model
|
||||
#derive_into_model
|
||||
))
|
||||
}
|
||||
|
||||
fn derive_active_model(all_fields: IntoIter<Field>) -> syn::Result<TokenStream> {
|
||||
let fields = all_fields.filter(field_not_ignored);
|
||||
|
||||
let field: Vec<Ident> = fields.clone().into_iter().map(format_field_ident).collect();
|
||||
|
||||
let name: Vec<Ident> = fields
|
||||
.clone()
|
||||
@ -143,3 +157,61 @@ pub fn expand_derive_active_model(ident: Ident, data: Data) -> syn::Result<Token
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
fn derive_into_model(model_fields: IntoIter<Field>) -> syn::Result<TokenStream> {
|
||||
let active_model_fields = model_fields.clone().filter(field_not_ignored);
|
||||
|
||||
let active_model_field: Vec<Ident> = active_model_fields
|
||||
.into_iter()
|
||||
.map(format_field_ident)
|
||||
.collect();
|
||||
let model_field: Vec<Ident> = model_fields
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(format_field_ident)
|
||||
.collect();
|
||||
|
||||
let ignore_attr: Vec<bool> = model_fields
|
||||
.map(|field| !field_not_ignored(&field))
|
||||
.collect();
|
||||
|
||||
let model_field_value: Vec<TokenStream> = model_field
|
||||
.iter()
|
||||
.zip(ignore_attr)
|
||||
.map(|(field, ignore)| {
|
||||
if ignore {
|
||||
quote! {
|
||||
Default::default()
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
a.#field.into_value().unwrap().unwrap()
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(quote!(
|
||||
#[automatically_derived]
|
||||
impl std::convert::TryFrom<ActiveModel> for <Entity as EntityTrait>::Model {
|
||||
type Error = DbErr;
|
||||
fn try_from(a: ActiveModel) -> Result<Self, DbErr> {
|
||||
#(if matches!(a.#active_model_field, sea_orm::ActiveValue::NotSet) {
|
||||
return Err(DbErr::AttrNotSet(stringify!(#active_model_field).to_owned()));
|
||||
})*
|
||||
Ok(
|
||||
Self {
|
||||
#(#model_field: #model_field_value),*
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[automatically_derived]
|
||||
impl sea_orm::TryIntoModel<<Entity as EntityTrait>::Model> for ActiveModel {
|
||||
fn try_into_model(self) -> Result<<Entity as EntityTrait>::Model, DbErr> {
|
||||
self.try_into()
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
use syn::{punctuated::Punctuated, token::Comma, Field, Meta};
|
||||
use quote::format_ident;
|
||||
use syn::{punctuated::Punctuated, token::Comma, Field, Ident, Meta};
|
||||
|
||||
pub(crate) fn field_not_ignored(field: &Field) -> bool {
|
||||
for attr in field.attrs.iter() {
|
||||
@ -25,6 +26,10 @@ pub(crate) fn field_not_ignored(field: &Field) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn format_field_ident(field: Field) -> Ident {
|
||||
format_ident!("{}", field.ident.unwrap().to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn trim_starting_raw_identifier<T>(string: T) -> String
|
||||
where
|
||||
T: ToString,
|
||||
|
@ -549,7 +549,7 @@ pub trait ActiveModelTrait: Clone + Debug {
|
||||
Ok(am)
|
||||
}
|
||||
|
||||
/// Return `true` if any field of `ActiveModel` is `Set`
|
||||
/// Return `true` if any attribute of `ActiveModel` is `Set`
|
||||
fn is_changed(&self) -> bool {
|
||||
<Self::Entity as EntityTrait>::Column::iter()
|
||||
.any(|col| self.get(col).is_set() && !self.get(col).is_unchanged())
|
||||
@ -947,6 +947,152 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "macros")]
|
||||
fn test_derive_try_into_model_1() {
|
||||
mod my_fruit {
|
||||
use crate as sea_orm;
|
||||
use crate::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "fruit")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub cake_id: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
}
|
||||
assert_eq!(
|
||||
my_fruit::ActiveModel {
|
||||
id: Set(1),
|
||||
name: Set("Pineapple".to_owned()),
|
||||
cake_id: Set(None),
|
||||
}
|
||||
.try_into_model()
|
||||
.unwrap(),
|
||||
my_fruit::Model {
|
||||
id: 1,
|
||||
name: "Pineapple".to_owned(),
|
||||
cake_id: None,
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
my_fruit::ActiveModel {
|
||||
id: Set(2),
|
||||
name: Set("Apple".to_owned()),
|
||||
cake_id: Set(Some(1)),
|
||||
}
|
||||
.try_into_model()
|
||||
.unwrap(),
|
||||
my_fruit::Model {
|
||||
id: 2,
|
||||
name: "Apple".to_owned(),
|
||||
cake_id: Some(1),
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
my_fruit::ActiveModel {
|
||||
id: Set(1),
|
||||
name: NotSet,
|
||||
cake_id: Set(None),
|
||||
}
|
||||
.try_into_model(),
|
||||
Err(DbErr::AttrNotSet(String::from("name")))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
my_fruit::ActiveModel {
|
||||
id: Set(1),
|
||||
name: Set("Pineapple".to_owned()),
|
||||
cake_id: NotSet,
|
||||
}
|
||||
.try_into_model(),
|
||||
Err(DbErr::AttrNotSet(String::from("cake_id")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "macros")]
|
||||
fn test_derive_try_into_model_2() {
|
||||
mod my_fruit {
|
||||
use crate as sea_orm;
|
||||
use crate::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "fruit")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
#[sea_orm(ignore)]
|
||||
pub cake_id: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
}
|
||||
assert_eq!(
|
||||
my_fruit::ActiveModel {
|
||||
id: Set(1),
|
||||
name: Set("Pineapple".to_owned()),
|
||||
}
|
||||
.try_into_model()
|
||||
.unwrap(),
|
||||
my_fruit::Model {
|
||||
id: 1,
|
||||
name: "Pineapple".to_owned(),
|
||||
cake_id: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "macros")]
|
||||
fn test_derive_try_into_model_3() {
|
||||
mod my_fruit {
|
||||
use crate as sea_orm;
|
||||
use crate::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "fruit")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
#[sea_orm(ignore)]
|
||||
pub name: String,
|
||||
pub cake_id: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
}
|
||||
assert_eq!(
|
||||
my_fruit::ActiveModel {
|
||||
id: Set(1),
|
||||
cake_id: Set(Some(1)),
|
||||
}
|
||||
.try_into_model()
|
||||
.unwrap(),
|
||||
my_fruit::Model {
|
||||
id: 1,
|
||||
name: "".to_owned(),
|
||||
cake_id: Some(1),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "with-json")]
|
||||
#[should_panic(
|
||||
@ -1105,12 +1251,12 @@ mod tests {
|
||||
Transaction::from_sql_and_values(
|
||||
DbBackend::Postgres,
|
||||
r#"INSERT INTO "fruit" ("name") VALUES ($1) RETURNING "id", "name", "cake_id""#,
|
||||
vec!["Apple".into()]
|
||||
vec!["Apple".into()],
|
||||
),
|
||||
Transaction::from_sql_and_values(
|
||||
DbBackend::Postgres,
|
||||
r#"UPDATE "fruit" SET "name" = $1, "cake_id" = $2 WHERE "fruit"."id" = $3 RETURNING "id", "name", "cake_id""#,
|
||||
vec!["Orange".into(), 1i32.into(), 2i32.into()]
|
||||
vec!["Orange".into(), 1i32.into(), 2i32.into()],
|
||||
),
|
||||
]
|
||||
);
|
||||
|
@ -115,3 +115,21 @@ pub trait FromQueryResult: Sized {
|
||||
SelectorRaw::<SelectModel<Self>>::from_statement(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Trait for any type that can be converted into an Model
|
||||
pub trait TryIntoModel<M>
|
||||
where
|
||||
M: ModelTrait,
|
||||
{
|
||||
/// Method to call to perform the conversion
|
||||
fn try_into_model(self) -> Result<M, DbErr>;
|
||||
}
|
||||
|
||||
impl<M> TryIntoModel<M> for M
|
||||
where
|
||||
M: ModelTrait,
|
||||
{
|
||||
fn try_into_model(self) -> Result<M, DbErr> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,9 @@ pub enum DbErr {
|
||||
/// The record was not found in the database
|
||||
#[error("RecordNotFound Error: {0}")]
|
||||
RecordNotFound(String),
|
||||
/// Thrown by `TryFrom<ActiveModel>`, which assumes all attributes are set/unchanged
|
||||
#[error("Attribute {0} is NotSet")]
|
||||
AttrNotSet(String),
|
||||
/// A custom error
|
||||
#[error("Custom Error: {0}")]
|
||||
Custom(String),
|
||||
|
Loading…
x
Reference in New Issue
Block a user