Update ActiveModel by JSON (#492)

* Update ActiveModel by JSON

* Add `ActiveModel::from_json`

* Update test cases
This commit is contained in:
Billy Chan 2022-03-13 18:41:32 +08:00 committed by GitHub
parent de57934061
commit 351efd0d6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 253 additions and 5 deletions

View File

@ -483,6 +483,71 @@ pub trait ActiveModelTrait: Clone + Debug {
ActiveModelBehavior::after_delete(am_clone)?; ActiveModelBehavior::after_delete(am_clone)?;
Ok(delete_res) Ok(delete_res)
} }
/// Set the corresponding attributes in the ActiveModel from a JSON value
///
/// Note that this method will not alter the primary key values in ActiveModel.
#[cfg(feature = "with-json")]
fn set_from_json(&mut self, json: serde_json::Value) -> Result<(), DbErr>
where
<<Self as ActiveModelTrait>::Entity as EntityTrait>::Model: IntoActiveModel<Self>,
for<'de> <<Self as ActiveModelTrait>::Entity as EntityTrait>::Model:
serde::de::Deserialize<'de>,
{
use crate::Iterable;
// Backup primary key values
let primary_key_values: Vec<(<Self::Entity as EntityTrait>::Column, ActiveValue<Value>)> =
<<Self::Entity as EntityTrait>::PrimaryKey>::iter()
.map(|pk| (pk.into_column(), self.take(pk.into_column())))
.collect();
// Replace all values in ActiveModel
*self = Self::from_json(json)?;
// Restore primary key values
for (col, active_value) in primary_key_values {
match active_value {
ActiveValue::Unchanged(v) | ActiveValue::Set(v) => self.set(col, v),
NotSet => self.not_set(col),
}
}
Ok(())
}
/// Create ActiveModel from a JSON value
#[cfg(feature = "with-json")]
fn from_json(json: serde_json::Value) -> Result<Self, DbErr>
where
<<Self as ActiveModelTrait>::Entity as EntityTrait>::Model: IntoActiveModel<Self>,
for<'de> <<Self as ActiveModelTrait>::Entity as EntityTrait>::Model:
serde::de::Deserialize<'de>,
{
use crate::{Iden, Iterable};
// Mark down which attribute exists in the JSON object
let json_keys: Vec<(<Self::Entity as EntityTrait>::Column, bool)> =
<<Self::Entity as EntityTrait>::Column>::iter()
.map(|col| (col, json.get(col.to_string()).is_some()))
.collect();
// Convert JSON object into ActiveModel via Model
let model: <Self::Entity as EntityTrait>::Model =
serde_json::from_value(json).map_err(|e| DbErr::Json(e.to_string()))?;
let mut am = model.into_active_model();
// Transform attribute that exists in JSON object into ActiveValue::Set, otherwise ActiveValue::NotSet
for (col, json_key_exists) in json_keys {
if json_key_exists && !am.is_not_set(col) {
am.set(col, am.get(col).unwrap());
} else {
am.not_set(col);
}
}
Ok(am)
}
} }
/// A Trait for overriding the ActiveModel behavior /// A Trait for overriding the ActiveModel behavior
@ -768,13 +833,15 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tests_cfg::*; use crate::{entity::*, tests_cfg::*, DbErr};
use pretty_assertions::assert_eq;
#[cfg(feature = "with-json")]
use serde_json::json;
#[test] #[test]
#[cfg(feature = "macros")] #[cfg(feature = "macros")]
fn test_derive_into_active_model_1() { fn test_derive_into_active_model_1() {
use crate::entity::*;
mod my_fruit { mod my_fruit {
pub use super::fruit::*; pub use super::fruit::*;
use crate as sea_orm; use crate as sea_orm;
@ -806,8 +873,6 @@ mod tests {
#[test] #[test]
#[cfg(feature = "macros")] #[cfg(feature = "macros")]
fn test_derive_into_active_model_2() { fn test_derive_into_active_model_2() {
use crate::entity::*;
mod my_fruit { mod my_fruit {
pub use super::fruit::*; pub use super::fruit::*;
use crate as sea_orm; use crate as sea_orm;
@ -852,4 +917,175 @@ mod tests {
} }
); );
} }
#[test]
#[cfg(feature = "with-json")]
#[should_panic(
expected = r#"called `Result::unwrap()` on an `Err` value: Json("missing field `id`")"#
)]
fn test_active_model_set_from_json_1() {
let mut cake: cake::ActiveModel = Default::default();
cake.set_from_json(json!({
"name": "Apple Pie",
}))
.unwrap();
}
#[test]
#[cfg(feature = "with-json")]
fn test_active_model_set_from_json_2() -> Result<(), DbErr> {
let mut fruit: fruit::ActiveModel = Default::default();
fruit.set_from_json(json!({
"name": "Apple",
}))?;
assert_eq!(
fruit,
fruit::ActiveModel {
id: ActiveValue::NotSet,
name: ActiveValue::Set("Apple".to_owned()),
cake_id: ActiveValue::NotSet,
}
);
assert_eq!(
fruit::ActiveModel::from_json(json!({
"name": "Apple",
}))?,
fruit::ActiveModel {
id: ActiveValue::NotSet,
name: ActiveValue::Set("Apple".to_owned()),
cake_id: ActiveValue::NotSet,
}
);
fruit.set_from_json(json!({
"name": "Apple",
"cake_id": null,
}))?;
assert_eq!(
fruit,
fruit::ActiveModel {
id: ActiveValue::NotSet,
name: ActiveValue::Set("Apple".to_owned()),
cake_id: ActiveValue::Set(None),
}
);
fruit.set_from_json(json!({
"id": null,
"name": "Apple",
"cake_id": 1,
}))?;
assert_eq!(
fruit,
fruit::ActiveModel {
id: ActiveValue::NotSet,
name: ActiveValue::Set("Apple".to_owned()),
cake_id: ActiveValue::Set(Some(1)),
}
);
fruit.set_from_json(json!({
"id": 2,
"name": "Apple",
"cake_id": 1,
}))?;
assert_eq!(
fruit,
fruit::ActiveModel {
id: ActiveValue::NotSet,
name: ActiveValue::Set("Apple".to_owned()),
cake_id: ActiveValue::Set(Some(1)),
}
);
let mut fruit = fruit::ActiveModel {
id: ActiveValue::Set(1),
name: ActiveValue::NotSet,
cake_id: ActiveValue::NotSet,
};
fruit.set_from_json(json!({
"id": 8,
"name": "Apple",
"cake_id": 1,
}))?;
assert_eq!(
fruit,
fruit::ActiveModel {
id: ActiveValue::Set(1),
name: ActiveValue::Set("Apple".to_owned()),
cake_id: ActiveValue::Set(Some(1)),
}
);
Ok(())
}
#[smol_potat::test]
#[cfg(feature = "with-json")]
async fn test_active_model_set_from_json_3() -> Result<(), DbErr> {
use crate::*;
let db = MockDatabase::new(DbBackend::Postgres)
.append_exec_results(vec![
MockExecResult {
last_insert_id: 1,
rows_affected: 1,
},
MockExecResult {
last_insert_id: 1,
rows_affected: 1,
},
])
.append_query_results(vec![
vec![fruit::Model {
id: 1,
name: "Apple".to_owned(),
cake_id: None,
}],
vec![fruit::Model {
id: 2,
name: "Orange".to_owned(),
cake_id: Some(1),
}],
])
.into_connection();
let mut fruit: fruit::ActiveModel = Default::default();
fruit.set_from_json(json!({
"name": "Apple",
}))?;
fruit.save(&db).await?;
let mut fruit = fruit::ActiveModel {
id: Set(2),
..Default::default()
};
fruit.set_from_json(json!({
"id": 9,
"name": "Orange",
"cake_id": 1,
}))?;
fruit.save(&db).await?;
assert_eq!(
db.into_transaction_log(),
vec![
Transaction::from_sql_and_values(
DbBackend::Postgres,
r#"INSERT INTO "fruit" ("name") VALUES ($1) RETURNING "id", "name", "cake_id""#,
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()]
),
]
);
Ok(())
}
} }

View File

@ -13,6 +13,8 @@ pub enum DbErr {
Custom(String), Custom(String),
/// Error occurred while parsing value as target type /// Error occurred while parsing value as target type
Type(String), Type(String),
/// Error occurred while parsing json value as target type
Json(String),
} }
impl std::error::Error for DbErr {} impl std::error::Error for DbErr {}
@ -26,6 +28,7 @@ impl std::fmt::Display for DbErr {
Self::RecordNotFound(s) => write!(f, "RecordNotFound Error: {}", s), Self::RecordNotFound(s) => write!(f, "RecordNotFound Error: {}", s),
Self::Custom(s) => write!(f, "Custom Error: {}", s), Self::Custom(s) => write!(f, "Custom Error: {}", s),
Self::Type(s) => write!(f, "Type Error: {}", s), Self::Type(s) => write!(f, "Type Error: {}", s),
Self::Json(s) => write!(f, "Json Error: {}", s),
} }
} }
} }

View File

@ -1,7 +1,11 @@
use crate as sea_orm; use crate as sea_orm;
use crate::entity::prelude::*; use crate::entity::prelude::*;
#[cfg(feature = "with-json")]
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[cfg_attr(feature = "with-json", derive(Serialize, Deserialize))]
#[sea_orm(table_name = "cake")] #[sea_orm(table_name = "cake")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]

View File

@ -1,10 +1,15 @@
use crate as sea_orm; use crate as sea_orm;
use crate::entity::prelude::*; use crate::entity::prelude::*;
#[cfg(feature = "with-json")]
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[cfg_attr(feature = "with-json", derive(Serialize, Deserialize))]
#[sea_orm(table_name = "fruit")] #[sea_orm(table_name = "fruit")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
#[serde(skip_deserializing)]
pub id: i32, pub id: i32,
pub name: String, pub name: String,
pub cake_id: Option<i32>, pub cake_id: Option<i32>,