From ab2f7847012ccf6463b737dd447220534730b5c6 Mon Sep 17 00:00:00 2001 From: Billy Chan Date: Fri, 1 Jul 2022 01:27:46 +0800 Subject: [PATCH] (de)serialize custom JSON types - 2 (#794) * de(serialize) custom JSON types * Rename DeriveTryGetableFromJson -> FromJsonQueryResult Co-authored-by: Chris Tsang --- sea-orm-macros/src/derives/mod.rs | 2 + .../src/derives/try_getable_from_json.rs | 44 ++++++++ sea-orm-macros/src/lib.rs | 10 ++ src/entity/prelude.rs | 2 +- src/executor/query.rs | 60 +++++++++++ src/lib.rs | 3 +- tests/common/features/json_struct.rs | 25 +++++ tests/common/features/mod.rs | 2 + tests/common/features/schema.rs | 23 ++++ tests/json_struct_tests.rs | 100 ++++++++++++++++++ 10 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 sea-orm-macros/src/derives/try_getable_from_json.rs create mode 100644 tests/common/features/json_struct.rs create mode 100644 tests/json_struct_tests.rs diff --git a/sea-orm-macros/src/derives/mod.rs b/sea-orm-macros/src/derives/mod.rs index f65b8358..295d3e7d 100644 --- a/sea-orm-macros/src/derives/mod.rs +++ b/sea-orm-macros/src/derives/mod.rs @@ -10,6 +10,7 @@ mod migration; mod model; mod primary_key; mod relation; +mod try_getable_from_json; pub use active_enum::*; pub use active_model::*; @@ -23,3 +24,4 @@ pub use migration::*; pub use model::*; pub use primary_key::*; pub use relation::*; +pub use try_getable_from_json::*; diff --git a/sea-orm-macros/src/derives/try_getable_from_json.rs b/sea-orm-macros/src/derives/try_getable_from_json.rs new file mode 100644 index 00000000..efcb76b5 --- /dev/null +++ b/sea-orm-macros/src/derives/try_getable_from_json.rs @@ -0,0 +1,44 @@ +use proc_macro2::{Ident, TokenStream}; +use quote::{format_ident, quote, quote_spanned}; +use syn::{ext::IdentExt, Data, DataStruct, Field, Fields}; + +pub fn expand_derive_from_json_query_result(ident: Ident, data: Data) -> syn::Result { + Ok(quote!( + #[automatically_derived] + impl sea_orm::TryGetableFromJson for #ident {} + + #[automatically_derived] + impl std::convert::From<#ident> for sea_orm::Value { + fn from(source: #ident) -> Self { + sea_orm::Value::Json(serde_json::to_value(&source).ok().map(|s| std::boxed::Box::new(s))) + } + } + + #[automatically_derived] + impl sea_query::ValueType for #ident { + fn try_from(v: sea_orm::Value) -> Result { + match v { + sea_orm::Value::Json(Some(json)) => Ok( + serde_json::from_value(*json).map_err(|_| sea_orm::sea_query::ValueTypeErr)?, + ), + _ => Err(sea_orm::sea_query::ValueTypeErr), + } + } + + fn type_name() -> String { + stringify!(#ident).to_owned() + } + + fn column_type() -> sea_orm::sea_query::ColumnType { + sea_orm::sea_query::ColumnType::Json + } + } + + #[automatically_derived] + impl sea_orm::sea_query::Nullable for #ident { + fn null() -> sea_orm::Value { + sea_orm::Value::Json(None) + } + } + )) +} diff --git a/sea-orm-macros/src/lib.rs b/sea-orm-macros/src/lib.rs index 64460d66..d9ea0d28 100644 --- a/sea-orm-macros/src/lib.rs +++ b/sea-orm-macros/src/lib.rs @@ -609,6 +609,16 @@ pub fn derive_migration_name(input: TokenStream) -> TokenStream { .into() } +#[proc_macro_derive(FromJsonQueryResult)] +pub fn derive_from_json_query_result(input: TokenStream) -> TokenStream { + let DeriveInput { ident, data, .. } = parse_macro_input!(input); + + match derives::expand_derive_from_json_query_result(ident, data) { + Ok(ts) => ts.into(), + Err(e) => e.to_compile_error().into(), + } +} + #[doc(hidden)] #[proc_macro_attribute] pub fn test(_: TokenStream, input: TokenStream) -> TokenStream { diff --git a/src/entity/prelude.rs b/src/entity/prelude.rs index 10b1bb50..ee181f38 100644 --- a/src/entity/prelude.rs +++ b/src/entity/prelude.rs @@ -9,7 +9,7 @@ pub use crate::{ pub use crate::{ DeriveActiveEnum, DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity, DeriveEntityModel, DeriveIntoActiveModel, DeriveModel, - DerivePrimaryKey, DeriveRelation, + DerivePrimaryKey, DeriveRelation, FromJsonQueryResult, }; #[cfg(feature = "with-json")] diff --git a/src/executor/query.rs b/src/executor/query.rs index 5d19aa1f..d7c2bb0d 100644 --- a/src/executor/query.rs +++ b/src/executor/query.rs @@ -584,6 +584,66 @@ fn try_get_many_with_slice_len_of(len: usize, cols: &[String]) -> Result<(), Try } } +// TryGetableFromJson // + +/// Perform a query on multiple columns +#[cfg(feature = "with-json")] +pub trait TryGetableFromJson: Sized +where + for<'de> Self: serde::Deserialize<'de>, +{ + /// Ensure the type implements this method + fn try_get_from_json(res: &QueryResult, pre: &str, col: &str) -> Result { + let column = format!("{}{}", pre, col); + let res = match &res.row { + #[cfg(feature = "sqlx-mysql")] + QueryResultRow::SqlxMySql(row) => { + use sqlx::Row; + row.try_get::, _>(column.as_str()) + .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) + .and_then(|opt| opt.ok_or(TryGetError::Null)) + } + #[cfg(feature = "sqlx-postgres")] + QueryResultRow::SqlxPostgres(row) => { + use sqlx::Row; + row.try_get::, _>(column.as_str()) + .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) + .and_then(|opt| opt.ok_or(TryGetError::Null)) + } + #[cfg(feature = "sqlx-sqlite")] + QueryResultRow::SqlxSqlite(row) => { + use sqlx::Row; + row.try_get::, _>(column.as_str()) + .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) + .and_then(|opt| opt.ok_or(TryGetError::Null)) + } + #[cfg(feature = "mock")] + QueryResultRow::Mock(row) => { + row.try_get::(column.as_str()) + .map_err(|e| { + debug_print!("{:#?}", e.to_string()); + TryGetError::Null + }) + } + #[allow(unreachable_patterns)] + _ => unreachable!(), + }; + res.and_then(|json| { + serde_json::from_value(json).map_err(|e| TryGetError::DbErr(DbErr::Json(e.to_string()))) + }) + } +} + +#[cfg(feature = "with-json")] +impl TryGetable for T +where + T: TryGetableFromJson, +{ + fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { + T::try_get_from_json(res, pre, col) + } +} + // TryFromU64 // /// Try to convert a type to a u64 pub trait TryFromU64: Sized { diff --git a/src/lib.rs b/src/lib.rs index c9848230..602514fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -330,7 +330,8 @@ pub use schema::*; pub use sea_orm_macros::{ DeriveActiveEnum, DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity, DeriveEntityModel, DeriveIntoActiveModel, - DeriveMigrationName, DeriveModel, DerivePrimaryKey, DeriveRelation, FromQueryResult, + DeriveMigrationName, DeriveModel, DerivePrimaryKey, DeriveRelation, FromJsonQueryResult, + FromQueryResult, }; pub use sea_query; diff --git a/tests/common/features/json_struct.rs b/tests/common/features/json_struct.rs new file mode 100644 index 00000000..44235d1b --- /dev/null +++ b/tests/common/features/json_struct.rs @@ -0,0 +1,25 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "json_struct")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub json: Json, + pub json_value: KeyValue, + pub json_value_opt: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, FromJsonQueryResult)] +pub struct KeyValue { + pub id: i32, + pub name: String, + pub price: f32, + pub notes: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/tests/common/features/mod.rs b/tests/common/features/mod.rs index fc9ac047..0b26b261 100644 --- a/tests/common/features/mod.rs +++ b/tests/common/features/mod.rs @@ -3,6 +3,7 @@ pub mod active_enum_child; pub mod applog; pub mod byte_primary_key; pub mod insert_default; +pub mod json_struct; pub mod json_vec; pub mod metadata; pub mod repository; @@ -17,6 +18,7 @@ pub use active_enum_child::Entity as ActiveEnumChild; pub use applog::Entity as Applog; pub use byte_primary_key::Entity as BytePrimaryKey; pub use insert_default::Entity as InsertDefault; +pub use json_struct::Entity as JsonStruct; pub use json_vec::Entity as JsonVec; pub use metadata::Entity as Metadata; pub use repository::Entity as Repository; diff --git a/tests/common/features/schema.rs b/tests/common/features/schema.rs index 0a654900..b6bf9ae4 100644 --- a/tests/common/features/schema.rs +++ b/tests/common/features/schema.rs @@ -19,6 +19,7 @@ pub async fn create_tables(db: &DatabaseConnection) -> Result<(), DbErr> { create_satellites_table(db).await?; create_transaction_log_table(db).await?; create_json_vec_table(db).await?; + create_json_struct_table(db).await?; let create_enum_stmts = match db_backend { DbBackend::MySql | DbBackend::Sqlite => Vec::new(), @@ -303,3 +304,25 @@ pub async fn create_json_vec_table(db: &DbConn) -> Result { create_table(db, &create_table_stmt, JsonVec).await } + +pub async fn create_json_struct_table(db: &DbConn) -> Result { + let stmt = sea_query::Table::create() + .table(json_struct::Entity) + .col( + ColumnDef::new(json_struct::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(json_struct::Column::Json).json().not_null()) + .col( + ColumnDef::new(json_struct::Column::JsonValue) + .json() + .not_null(), + ) + .col(ColumnDef::new(json_struct::Column::JsonValueOpt).json()) + .to_owned(); + + create_table(db, &stmt, JsonStruct).await +} diff --git a/tests/json_struct_tests.rs b/tests/json_struct_tests.rs new file mode 100644 index 00000000..5c587690 --- /dev/null +++ b/tests/json_struct_tests.rs @@ -0,0 +1,100 @@ +pub mod common; + +pub use common::{features::*, setup::*, TestContext}; +use pretty_assertions::assert_eq; +use sea_orm::{entity::prelude::*, entity::*, DatabaseConnection}; +use serde_json::json; + +#[sea_orm_macros::test] +#[cfg(any( + feature = "sqlx-mysql", + feature = "sqlx-sqlite", + feature = "sqlx-postgres" +))] +async fn main() -> Result<(), DbErr> { + let ctx = TestContext::new("json_struct_tests").await; + create_tables(&ctx.db).await?; + insert_json_struct_1(&ctx.db).await?; + insert_json_struct_2(&ctx.db).await?; + ctx.delete().await; + + Ok(()) +} + +pub async fn insert_json_struct_1(db: &DatabaseConnection) -> Result<(), DbErr> { + use json_struct::*; + + let model = Model { + id: 1, + json: json!({ + "id": 1, + "name": "apple", + "price": 12.01, + "notes": "hand picked, organic", + }), + json_value: KeyValue { + id: 1, + name: "apple".into(), + price: 12.01, + notes: Some("hand picked, organic".into()), + } + .into(), + json_value_opt: Some(KeyValue { + id: 1, + name: "apple".into(), + price: 12.01, + notes: Some("hand picked, organic".into()), + }) + .into(), + }; + + let result = model.clone().into_active_model().insert(db).await?; + + assert_eq!(result, model); + + assert_eq!( + Entity::find() + .filter(Column::Id.eq(model.id)) + .one(db) + .await?, + Some(model) + ); + + Ok(()) +} + +pub async fn insert_json_struct_2(db: &DatabaseConnection) -> Result<(), DbErr> { + use json_struct::*; + + let model = Model { + id: 2, + json: json!({ + "id": 2, + "name": "orange", + "price": 10.93, + "notes": "sweet & juicy", + }), + json_value: KeyValue { + id: 1, + name: "orange".into(), + price: 10.93, + notes: None, + } + .into(), + json_value_opt: None.into(), + }; + + let result = model.clone().into_active_model().insert(db).await?; + + assert_eq!(result, model); + + assert_eq!( + Entity::find() + .filter(Column::Id.eq(model.id)) + .one(db) + .await?, + Some(model) + ); + + Ok(()) +}