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:
Billy Chan 2022-10-23 23:12:22 +08:00 committed by GitHub
parent 3f07bd19b1
commit a766500ebf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 258 additions and 14 deletions

View File

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

View File

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

View File

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

View File

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

View File

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