diff --git a/CHANGELOG.md b/CHANGELOG.md index 89fdbcb57..251f349c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### New Features * Better error types (carrying SQLx Error) https://github.com/SeaQL/sea-orm/pull/1002 +* Support array datatype in PostgreSQL https://github.com/SeaQL/sea-orm/pull/1132 * [sea-orm-cli] Generate entity files as a library or module https://github.com/SeaQL/sea-orm/pull/953 * [sea-orm-cli] Generate a new migration template with name prefix of unix timestamp https://github.com/SeaQL/sea-orm/pull/947 * [sea-orm-cli] Generate migration in modules https://github.com/SeaQL/sea-orm/pull/933 diff --git a/sea-orm-macros/src/derives/active_model.rs b/sea-orm-macros/src/derives/active_model.rs index 635897dcb..eed3dac0c 100644 --- a/sea-orm-macros/src/derives/active_model.rs +++ b/sea-orm-macros/src/derives/active_model.rs @@ -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 { - 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 = 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) -> syn::Result { + let fields = all_fields.filter(field_not_ignored); + + let field: Vec = fields.clone().into_iter().map(format_field_ident).collect(); let name: Vec = fields .clone() @@ -143,3 +157,61 @@ pub fn expand_derive_active_model(ident: Ident, data: Data) -> syn::Result) -> syn::Result { + let active_model_fields = model_fields.clone().filter(field_not_ignored); + + let active_model_field: Vec = active_model_fields + .into_iter() + .map(format_field_ident) + .collect(); + let model_field: Vec = model_fields + .clone() + .into_iter() + .map(format_field_ident) + .collect(); + + let ignore_attr: Vec = model_fields + .map(|field| !field_not_ignored(&field)) + .collect(); + + let model_field_value: Vec = 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 for ::Model { + type Error = DbErr; + fn try_from(a: ActiveModel) -> Result { + #(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<::Model> for ActiveModel { + fn try_into_model(self) -> Result<::Model, DbErr> { + self.try_into() + } + } + )) +} diff --git a/sea-orm-macros/src/util.rs b/sea-orm-macros/src/util.rs index 379b486ca..38b5a64c6 100644 --- a/sea-orm-macros/src/util.rs +++ b/sea-orm-macros/src/util.rs @@ -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(string: T) -> String where T: ToString, diff --git a/src/entity/active_model.rs b/src/entity/active_model.rs index e63cbea4b..2da20c4a8 100644 --- a/src/entity/active_model.rs +++ b/src/entity/active_model.rs @@ -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 { ::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, + } + + #[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, + } + + #[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, + } + + #[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()], ), ] ); diff --git a/src/entity/model.rs b/src/entity/model.rs index 34520dc3d..3869ec52f 100644 --- a/src/entity/model.rs +++ b/src/entity/model.rs @@ -115,3 +115,21 @@ pub trait FromQueryResult: Sized { SelectorRaw::>::from_statement(stmt) } } + +/// A Trait for any type that can be converted into an Model +pub trait TryIntoModel +where + M: ModelTrait, +{ + /// Method to call to perform the conversion + fn try_into_model(self) -> Result; +} + +impl TryIntoModel for M +where + M: ModelTrait, +{ + fn try_into_model(self) -> Result { + Ok(self) + } +} diff --git a/src/error.rs b/src/error.rs index 01c4f21ff..58e6a1a7f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -40,6 +40,9 @@ pub enum DbErr { /// The record was not found in the database #[error("RecordNotFound Error: {0}")] RecordNotFound(String), + /// Thrown by `TryFrom`, which assumes all attributes are set/unchanged + #[error("Attribute {0} is NotSet")] + AttrNotSet(String), /// A custom error #[error("Custom Error: {0}")] Custom(String),