(de)serialize custom JSON types - 2 (#794)

* de(serialize) custom JSON types

* Rename DeriveTryGetableFromJson -> FromJsonQueryResult

Co-authored-by: Chris Tsang <chris.2y3@outlook.com>
This commit is contained in:
Billy Chan 2022-07-01 01:27:46 +08:00 committed by GitHub
parent 4be32e7a9f
commit ab2f784701
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 269 additions and 2 deletions

View File

@ -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::*;

View File

@ -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<TokenStream> {
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<Self, sea_orm::sea_query::ValueTypeErr> {
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)
}
}
))
}

View File

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

View File

@ -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")]

View File

@ -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<Self, TryGetError> {
let column = format!("{}{}", pre, col);
let res = match &res.row {
#[cfg(feature = "sqlx-mysql")]
QueryResultRow::SqlxMySql(row) => {
use sqlx::Row;
row.try_get::<Option<serde_json::Value>, _>(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::<Option<serde_json::Value>, _>(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::<Option<serde_json::Value>, _>(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::<serde_json::Value>(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<T> TryGetable for T
where
T: TryGetableFromJson,
{
fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result<Self, TryGetError> {
T::try_get_from_json(res, pre, col)
}
}
// TryFromU64 //
/// Try to convert a type to a u64
pub trait TryFromU64: Sized {

View File

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

View File

@ -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<KeyValue>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, FromJsonQueryResult)]
pub struct KeyValue {
pub id: i32,
pub name: String,
pub price: f32,
pub notes: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

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

View File

@ -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<ExecResult, DbErr> {
create_table(db, &create_table_stmt, JsonVec).await
}
pub async fn create_json_struct_table(db: &DbConn) -> Result<ExecResult, DbErr> {
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
}

100
tests/json_struct_tests.rs Normal file
View File

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