From 6d775e234dfc389a94d4ec8a16bb0378589b78e0 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Fri, 20 Oct 2023 12:44:23 +0300 Subject: [PATCH 1/8] Implement deriving `Insertable` with `serialize_fn`. --- diesel_derives/src/attrs.rs | 19 +++++- diesel_derives/src/field.rs | 20 ++++++ diesel_derives/src/insertable.rs | 99 ++++++++++++++++++++++++++++-- diesel_derives/src/util.rs | 2 + diesel_derives/tests/insertable.rs | 36 +++++++++++ 5 files changed, 169 insertions(+), 7 deletions(-) diff --git a/diesel_derives/src/attrs.rs b/diesel_derives/src/attrs.rs index c7d82fe250a9..61f1f6244be7 100644 --- a/diesel_derives/src/attrs.rs +++ b/diesel_derives/src/attrs.rs @@ -13,9 +13,10 @@ use crate::deprecated::ParseDeprecated; use crate::parsers::{BelongsTo, MysqlType, PostgresType, SqliteType}; use crate::util::{ parse_eq, parse_paren, unknown_attribute, BELONGS_TO_NOTE, COLUMN_NAME_NOTE, - DESERIALIZE_AS_NOTE, MYSQL_TYPE_NOTE, POSTGRES_TYPE_NOTE, SELECT_EXPRESSION_NOTE, - SELECT_EXPRESSION_TYPE_NOTE, SERIALIZE_AS_NOTE, SQLITE_TYPE_NOTE, SQL_TYPE_NOTE, - TABLE_NAME_NOTE, TREAT_NONE_AS_DEFAULT_VALUE_NOTE, TREAT_NONE_AS_NULL_NOTE, + DESERIALIZE_AS_NOTE, DESERIALIZE_FN_NOTE, MYSQL_TYPE_NOTE, POSTGRES_TYPE_NOTE, + SELECT_EXPRESSION_NOTE, SELECT_EXPRESSION_TYPE_NOTE, SERIALIZE_AS_NOTE, SERIALIZE_FN_NOTE, + SQLITE_TYPE_NOTE, SQL_TYPE_NOTE, TABLE_NAME_NOTE, TREAT_NONE_AS_DEFAULT_VALUE_NOTE, + TREAT_NONE_AS_NULL_NOTE, }; use crate::util::{parse_paren_list, CHECK_FOR_BACKEND_NOTE}; @@ -40,6 +41,8 @@ pub enum FieldAttr { SerializeAs(Ident, TypePath), DeserializeAs(Ident, TypePath), + SerializeFn(Ident, Expr), + DeserializeFn(Ident, Expr), SelectExpression(Ident, Expr), SelectExpressionType(Ident, Type), } @@ -145,6 +148,14 @@ impl Parse for FieldAttr { name, parse_eq(input, DESERIALIZE_AS_NOTE)?, )), + "serialize_fn" => Ok(FieldAttr::SerializeFn( + name, + parse_eq(input, SERIALIZE_FN_NOTE)?, + )), + "deserialize_fn" => Ok(FieldAttr::DeserializeFn( + name, + parse_eq(input, DESERIALIZE_FN_NOTE)?, + )), "select_expression" => Ok(FieldAttr::SelectExpression( name, parse_eq(input, SELECT_EXPRESSION_NOTE)?, @@ -179,6 +190,8 @@ impl MySpanned for FieldAttr { | FieldAttr::TreatNoneAsDefaultValue(ident, _) | FieldAttr::SerializeAs(ident, _) | FieldAttr::DeserializeAs(ident, _) + | FieldAttr::SerializeFn(ident, _) + | FieldAttr::DeserializeFn(ident, _) | FieldAttr::SelectExpression(ident, _) | FieldAttr::SelectExpressionType(ident, _) => ident.span(), } diff --git a/diesel_derives/src/field.rs b/diesel_derives/src/field.rs index 88a0d8cbe17c..ff4fbceb61b5 100644 --- a/diesel_derives/src/field.rs +++ b/diesel_derives/src/field.rs @@ -14,6 +14,8 @@ pub struct Field { pub treat_none_as_null: Option>, pub serialize_as: Option>, pub deserialize_as: Option>, + pub serialize_fn: Option>, + pub deserialize_fn: Option>, pub select_expression: Option>, pub select_expression_type: Option>, pub embed: Option>, @@ -29,6 +31,8 @@ impl Field { let mut sql_type = None; let mut serialize_as = None; let mut deserialize_as = None; + let mut serialize_fn = None; + let mut deserialize_fn = None; let mut embed = None; let mut select_expression = None; let mut select_expression_type = None; @@ -81,6 +85,20 @@ impl Field { ident_span, }) } + FieldAttr::SerializeFn(_, value) => { + serialize_fn = Some(AttributeSpanWrapper { + item: value, + attribute_span, + ident_span, + }) + } + FieldAttr::DeserializeFn(_, value) => { + deserialize_fn = Some(AttributeSpanWrapper { + item: value, + attribute_span, + ident_span, + }) + } FieldAttr::SelectExpression(_, value) => { select_expression = Some(AttributeSpanWrapper { item: value, @@ -125,6 +143,8 @@ impl Field { treat_none_as_null, serialize_as, deserialize_as, + serialize_fn, + deserialize_fn, select_expression, select_expression_type, embed, diff --git a/diesel_derives/src/insertable.rs b/diesel_derives/src/insertable.rs index f1774bc43cc3..7f56989df96c 100644 --- a/diesel_derives/src/insertable.rs +++ b/diesel_derives/src/insertable.rs @@ -107,14 +107,82 @@ fn derive_into_single_table( ty, treat_none_as_default_value, )?); - direct_field_assign.push(field_expr_serialize_as( + if field.serialize_fn.is_none() { + direct_field_assign.push(field_expr_serialize_as( + field, + table_name, + ty, + treat_none_as_default_value, + )?); + } + + generate_borrowed_insert = false; // as soon as we hit one field with #[diesel(serialize_as)] there is no point in generating the impl of Insertable for borrowed structs + } + (Some(AttributeSpanWrapper { attribute_span, .. }), true) => { + return Err(syn::Error::new( + *attribute_span, + "`#[diesel(embed)]` cannot be combined with `#[diesel(serialize_as)]`", + )); + } + } + + match (field.serialize_fn.as_ref(), field.embed()) { + (None, true) => { + direct_field_ty.push(field_ty_embed(field, None)); + direct_field_assign.push(field_expr_embed(field, None)); + ref_field_ty.push(field_ty_embed(field, Some(quote!(&'insert)))); + ref_field_assign.push(field_expr_embed(field, Some(quote!(&)))); + } + (None, false) => { + direct_field_ty.push(field_ty( field, table_name, - ty, + None, + treat_none_as_default_value, + )?); + direct_field_assign.push(field_expr( + field, + table_name, + None, treat_none_as_default_value, )?); + ref_field_ty.push(field_ty( + field, + table_name, + Some(quote!(&'insert)), + treat_none_as_default_value, + )?); + ref_field_assign.push(field_expr( + field, + table_name, + Some(quote!(&)), + treat_none_as_default_value, + )?); + } + ( + Some(AttributeSpanWrapper { + item: function, + attribute_span, + .. + }), + false, + ) => { + if let Some(AttributeSpanWrapper { item: ty, .. }) = field.serialize_as.as_ref() { + direct_field_assign.push(field_expr_serialize_fn( + field, + table_name, + ty, + function, + treat_none_as_default_value, + )?); - generate_borrowed_insert = false; // as soon as we hit one field with #[diesel(serialize_as)] there is no point in generating the impl of Insertable for borrowed structs + generate_borrowed_insert = false; // as soon as we hit one field with #[diesel(serialize_fn)] there is no point in generating the impl of Insertable for borrowed structs + } else { + return Err(syn::Error::new( + *attribute_span, + "`#[diesel(serialize_fn)]` reqires `#[diesel(serialize_as)]` to be set for the same field", + )); + } } (Some(AttributeSpanWrapper { attribute_span, .. }), true) => { return Err(syn::Error::new( @@ -227,7 +295,7 @@ fn field_expr_serialize_as( Ok(quote!(self.#field_name.map(|x| #column.eq(::std::convert::Into::<#ty>::into(x))))) } else { Ok( - quote!(std::option::Option::Some(#column.eq(::std::convert::Into::<#ty>::into(self.#field_name)))), + quote!(::std::option::Option::Some(#column.eq(::std::convert::Into::<#ty>::into(self.#field_name)))), ) } } else { @@ -235,6 +303,29 @@ fn field_expr_serialize_as( } } +fn field_expr_serialize_fn( + field: &Field, + table_name: &Path, + ty: &Type, + function: &Expr, + treat_none_as_default_value: bool, +) -> Result { + let field_name = &field.name; + let column_name = field.column_name()?; + column_name.valid_ident()?; + let column = quote!(#table_name::#column_name); + + if treat_none_as_default_value { + if is_option_ty(ty) { + Ok(quote!(self.#field_name.map(|x| #column.eq((#function)(x))))) + } else { + Ok(quote!(::std::option::Option::Some(#column.eq((#function)(self.#field_name))))) + } + } else { + Ok(quote!(#column.eq((#function)(self.#field_name)))) + } +} + fn field_ty( field: &Field, table_name: &Path, diff --git a/diesel_derives/src/util.rs b/diesel_derives/src/util.rs index 4f9930fcc113..8cbbd1525284 100644 --- a/diesel_derives/src/util.rs +++ b/diesel_derives/src/util.rs @@ -10,6 +10,8 @@ pub const COLUMN_NAME_NOTE: &str = "column_name = foo"; pub const SQL_TYPE_NOTE: &str = "sql_type = Foo"; pub const SERIALIZE_AS_NOTE: &str = "serialize_as = Foo"; pub const DESERIALIZE_AS_NOTE: &str = "deserialize_as = Foo"; +pub const SERIALIZE_FN_NOTE: &str = "serialize_fn = some_function"; +pub const DESERIALIZE_FN_NOTE: &str = "deserialize_fn = some_function"; pub const TABLE_NAME_NOTE: &str = "table_name = foo"; pub const TREAT_NONE_AS_DEFAULT_VALUE_NOTE: &str = "treat_none_as_default_value = true"; pub const TREAT_NONE_AS_NULL_NOTE: &str = "treat_none_as_null = true"; diff --git a/diesel_derives/tests/insertable.rs b/diesel_derives/tests/insertable.rs index d6eaeef83ac2..93f31c92b53f 100644 --- a/diesel_derives/tests/insertable.rs +++ b/diesel_derives/tests/insertable.rs @@ -377,3 +377,39 @@ fn embedded_struct() { let expected = vec![(1, "Sean".to_string(), Some("Black".to_string()))]; assert_eq!(Ok(expected), saved); } + +#[test] +fn serialize_fn_custom_option_field() { + #[derive(Debug, Clone)] + struct UserName(String); + impl From for String { + fn from(value: UserName) -> Self { + value.0 + } + } + + #[derive(Insertable, Debug, Clone)] + #[diesel(table_name = users)] + #[diesel(treat_none_as_default_value = false)] + struct NewUser { + #[diesel(serialize_as = String)] + name: UserName, + hair_color: Option, + } + + let conn = &mut connection(); + let new_user = NewUser { + name: "Sean".into(), + hair_color: "Black".into(), + }; + insert_into(users::table) + .values(&new_user) + .execute(conn) + .unwrap(); + + let saved = users::table + .select((users::name, users::hair_color)) + .load::<(String, Option)>(conn); + let expected = vec![("Sean".to_string(), Some("Black".to_string()))]; + assert_eq!(Ok(expected), saved); +} From 97a8802da9c2cce6d3ab32eb9df88deebc8fec2c Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Fri, 20 Oct 2023 13:13:20 +0300 Subject: [PATCH 2/8] Fix `Insertable` macro generating duplicate derivations. --- diesel_derives/src/insertable.rs | 92 +++++++++----------------------- 1 file changed, 25 insertions(+), 67 deletions(-) diff --git a/diesel_derives/src/insertable.rs b/diesel_derives/src/insertable.rs index 7f56989df96c..5e4de836140f 100644 --- a/diesel_derives/src/insertable.rs +++ b/diesel_derives/src/insertable.rs @@ -67,14 +67,18 @@ fn derive_into_single_table( None => treat_none_as_default_value, }; - match (field.serialize_as.as_ref(), field.embed()) { - (None, true) => { + match ( + field.serialize_as.as_ref(), + field.serialize_fn.as_ref(), + field.embed(), + ) { + (None, None, true) => { direct_field_ty.push(field_ty_embed(field, None)); direct_field_assign.push(field_expr_embed(field, None)); ref_field_ty.push(field_ty_embed(field, Some(quote!(&'insert)))); ref_field_assign.push(field_expr_embed(field, Some(quote!(&)))); } - (None, false) => { + (None, None, false) => { direct_field_ty.push(field_ty( field, table_name, @@ -100,14 +104,22 @@ fn derive_into_single_table( treat_none_as_default_value, )?); } - (Some(AttributeSpanWrapper { item: ty, .. }), false) => { + (Some(AttributeSpanWrapper { item: ty, .. }), serialize_fn, false) => { direct_field_ty.push(field_ty_serialize_as( field, table_name, ty, treat_none_as_default_value, )?); - if field.serialize_fn.is_none() { + if let Some(AttributeSpanWrapper { item: function, .. }) = serialize_fn { + direct_field_assign.push(field_expr_serialize_fn( + field, + table_name, + ty, + function, + treat_none_as_default_value, + )?); + } else { direct_field_assign.push(field_expr_serialize_as( field, table_name, @@ -118,76 +130,22 @@ fn derive_into_single_table( generate_borrowed_insert = false; // as soon as we hit one field with #[diesel(serialize_as)] there is no point in generating the impl of Insertable for borrowed structs } - (Some(AttributeSpanWrapper { attribute_span, .. }), true) => { + (Some(AttributeSpanWrapper { attribute_span, .. }), _, true) => { return Err(syn::Error::new( *attribute_span, "`#[diesel(embed)]` cannot be combined with `#[diesel(serialize_as)]`", )); } - } - - match (field.serialize_fn.as_ref(), field.embed()) { - (None, true) => { - direct_field_ty.push(field_ty_embed(field, None)); - direct_field_assign.push(field_expr_embed(field, None)); - ref_field_ty.push(field_ty_embed(field, Some(quote!(&'insert)))); - ref_field_assign.push(field_expr_embed(field, Some(quote!(&)))); - } - (None, false) => { - direct_field_ty.push(field_ty( - field, - table_name, - None, - treat_none_as_default_value, - )?); - direct_field_assign.push(field_expr( - field, - table_name, - None, - treat_none_as_default_value, - )?); - ref_field_ty.push(field_ty( - field, - table_name, - Some(quote!(&'insert)), - treat_none_as_default_value, - )?); - ref_field_assign.push(field_expr( - field, - table_name, - Some(quote!(&)), - treat_none_as_default_value, - )?); - } - ( - Some(AttributeSpanWrapper { - item: function, - attribute_span, - .. - }), - false, - ) => { - if let Some(AttributeSpanWrapper { item: ty, .. }) = field.serialize_as.as_ref() { - direct_field_assign.push(field_expr_serialize_fn( - field, - table_name, - ty, - function, - treat_none_as_default_value, - )?); - - generate_borrowed_insert = false; // as soon as we hit one field with #[diesel(serialize_fn)] there is no point in generating the impl of Insertable for borrowed structs - } else { - return Err(syn::Error::new( - *attribute_span, - "`#[diesel(serialize_fn)]` reqires `#[diesel(serialize_as)]` to be set for the same field", - )); - } + (None, Some(AttributeSpanWrapper { attribute_span, .. }), true) => { + return Err(syn::Error::new( + *attribute_span, + "`#[diesel(embed)]` cannot be combined with `#[diesel(serialize_fn)]`", + )); } - (Some(AttributeSpanWrapper { attribute_span, .. }), true) => { + (None, Some(AttributeSpanWrapper { attribute_span, .. }), false) => { return Err(syn::Error::new( *attribute_span, - "`#[diesel(embed)]` cannot be combined with `#[diesel(serialize_as)]`", + "`#[diesel(serialize_fn)]` requires `#[diesel(serialize_as)]` to be declared as well", )); } } From 069d0f00016d066aa472dde531147fd45a182cca Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Fri, 20 Oct 2023 13:32:52 +0300 Subject: [PATCH 3/8] Fix `serialize_fn` test. --- diesel_derives/tests/insertable.rs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/diesel_derives/tests/insertable.rs b/diesel_derives/tests/insertable.rs index 93f31c92b53f..612ab6fc4df8 100644 --- a/diesel_derives/tests/insertable.rs +++ b/diesel_derives/tests/insertable.rs @@ -380,7 +380,6 @@ fn embedded_struct() { #[test] fn serialize_fn_custom_option_field() { - #[derive(Debug, Clone)] struct UserName(String); impl From for String { fn from(value: UserName) -> Self { @@ -388,28 +387,41 @@ fn serialize_fn_custom_option_field() { } } - #[derive(Insertable, Debug, Clone)] + enum HairColor { + Green, + } + impl From for String { + fn from(value: HairColor) -> Self { + match value { + HairColor::Green => "Green".into(), + } + } + } + + #[derive(Insertable)] #[diesel(table_name = users)] #[diesel(treat_none_as_default_value = false)] struct NewUser { #[diesel(serialize_as = String)] name: UserName, - hair_color: Option, + #[diesel(serialize_as = Option)] + #[diesel(serialize_fn = |x: Option| x.map(Into::into))] + hair_color: Option, } let conn = &mut connection(); let new_user = NewUser { - name: "Sean".into(), - hair_color: "Black".into(), + name: UserName("Sean".into()), + hair_color: Some(HairColor::Green), }; insert_into(users::table) - .values(&new_user) + .values(new_user) .execute(conn) .unwrap(); let saved = users::table .select((users::name, users::hair_color)) .load::<(String, Option)>(conn); - let expected = vec![("Sean".to_string(), Some("Black".to_string()))]; + let expected = vec![("Sean".to_string(), Some("Green".to_string()))]; assert_eq!(Ok(expected), saved); } From 8299a48a52a21345ef01362374e4d4e60d317b15 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Fri, 20 Oct 2023 13:41:40 +0300 Subject: [PATCH 4/8] Remove `deserialize_fn`. --- diesel_derives/src/attrs.rs | 13 +++---------- diesel_derives/src/field.rs | 10 ---------- diesel_derives/src/util.rs | 1 - 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/diesel_derives/src/attrs.rs b/diesel_derives/src/attrs.rs index 61f1f6244be7..9b45d9a835b4 100644 --- a/diesel_derives/src/attrs.rs +++ b/diesel_derives/src/attrs.rs @@ -13,10 +13,9 @@ use crate::deprecated::ParseDeprecated; use crate::parsers::{BelongsTo, MysqlType, PostgresType, SqliteType}; use crate::util::{ parse_eq, parse_paren, unknown_attribute, BELONGS_TO_NOTE, COLUMN_NAME_NOTE, - DESERIALIZE_AS_NOTE, DESERIALIZE_FN_NOTE, MYSQL_TYPE_NOTE, POSTGRES_TYPE_NOTE, - SELECT_EXPRESSION_NOTE, SELECT_EXPRESSION_TYPE_NOTE, SERIALIZE_AS_NOTE, SERIALIZE_FN_NOTE, - SQLITE_TYPE_NOTE, SQL_TYPE_NOTE, TABLE_NAME_NOTE, TREAT_NONE_AS_DEFAULT_VALUE_NOTE, - TREAT_NONE_AS_NULL_NOTE, + DESERIALIZE_AS_NOTE, MYSQL_TYPE_NOTE, POSTGRES_TYPE_NOTE, SELECT_EXPRESSION_NOTE, + SELECT_EXPRESSION_TYPE_NOTE, SERIALIZE_AS_NOTE, SERIALIZE_FN_NOTE, SQLITE_TYPE_NOTE, + SQL_TYPE_NOTE, TABLE_NAME_NOTE, TREAT_NONE_AS_DEFAULT_VALUE_NOTE, TREAT_NONE_AS_NULL_NOTE, }; use crate::util::{parse_paren_list, CHECK_FOR_BACKEND_NOTE}; @@ -42,7 +41,6 @@ pub enum FieldAttr { SerializeAs(Ident, TypePath), DeserializeAs(Ident, TypePath), SerializeFn(Ident, Expr), - DeserializeFn(Ident, Expr), SelectExpression(Ident, Expr), SelectExpressionType(Ident, Type), } @@ -152,10 +150,6 @@ impl Parse for FieldAttr { name, parse_eq(input, SERIALIZE_FN_NOTE)?, )), - "deserialize_fn" => Ok(FieldAttr::DeserializeFn( - name, - parse_eq(input, DESERIALIZE_FN_NOTE)?, - )), "select_expression" => Ok(FieldAttr::SelectExpression( name, parse_eq(input, SELECT_EXPRESSION_NOTE)?, @@ -191,7 +185,6 @@ impl MySpanned for FieldAttr { | FieldAttr::SerializeAs(ident, _) | FieldAttr::DeserializeAs(ident, _) | FieldAttr::SerializeFn(ident, _) - | FieldAttr::DeserializeFn(ident, _) | FieldAttr::SelectExpression(ident, _) | FieldAttr::SelectExpressionType(ident, _) => ident.span(), } diff --git a/diesel_derives/src/field.rs b/diesel_derives/src/field.rs index ff4fbceb61b5..624d78726b03 100644 --- a/diesel_derives/src/field.rs +++ b/diesel_derives/src/field.rs @@ -15,7 +15,6 @@ pub struct Field { pub serialize_as: Option>, pub deserialize_as: Option>, pub serialize_fn: Option>, - pub deserialize_fn: Option>, pub select_expression: Option>, pub select_expression_type: Option>, pub embed: Option>, @@ -32,7 +31,6 @@ impl Field { let mut serialize_as = None; let mut deserialize_as = None; let mut serialize_fn = None; - let mut deserialize_fn = None; let mut embed = None; let mut select_expression = None; let mut select_expression_type = None; @@ -92,13 +90,6 @@ impl Field { ident_span, }) } - FieldAttr::DeserializeFn(_, value) => { - deserialize_fn = Some(AttributeSpanWrapper { - item: value, - attribute_span, - ident_span, - }) - } FieldAttr::SelectExpression(_, value) => { select_expression = Some(AttributeSpanWrapper { item: value, @@ -144,7 +135,6 @@ impl Field { serialize_as, deserialize_as, serialize_fn, - deserialize_fn, select_expression, select_expression_type, embed, diff --git a/diesel_derives/src/util.rs b/diesel_derives/src/util.rs index 8cbbd1525284..b43ce79f4cb3 100644 --- a/diesel_derives/src/util.rs +++ b/diesel_derives/src/util.rs @@ -11,7 +11,6 @@ pub const SQL_TYPE_NOTE: &str = "sql_type = Foo"; pub const SERIALIZE_AS_NOTE: &str = "serialize_as = Foo"; pub const DESERIALIZE_AS_NOTE: &str = "deserialize_as = Foo"; pub const SERIALIZE_FN_NOTE: &str = "serialize_fn = some_function"; -pub const DESERIALIZE_FN_NOTE: &str = "deserialize_fn = some_function"; pub const TABLE_NAME_NOTE: &str = "table_name = foo"; pub const TREAT_NONE_AS_DEFAULT_VALUE_NOTE: &str = "treat_none_as_default_value = true"; pub const TREAT_NONE_AS_NULL_NOTE: &str = "treat_none_as_null = true"; From 7369b3a65ef01c5231063ba3b44ba684eeee75f8 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Fri, 27 Oct 2023 10:33:35 +0300 Subject: [PATCH 5/8] Revert "Remove `deserialize_fn`." This reverts commit 8299a48a52a21345ef01362374e4d4e60d317b15. --- diesel_derives/src/attrs.rs | 13 ++++++++++--- diesel_derives/src/field.rs | 10 ++++++++++ diesel_derives/src/util.rs | 1 + 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/diesel_derives/src/attrs.rs b/diesel_derives/src/attrs.rs index 9b45d9a835b4..61f1f6244be7 100644 --- a/diesel_derives/src/attrs.rs +++ b/diesel_derives/src/attrs.rs @@ -13,9 +13,10 @@ use crate::deprecated::ParseDeprecated; use crate::parsers::{BelongsTo, MysqlType, PostgresType, SqliteType}; use crate::util::{ parse_eq, parse_paren, unknown_attribute, BELONGS_TO_NOTE, COLUMN_NAME_NOTE, - DESERIALIZE_AS_NOTE, MYSQL_TYPE_NOTE, POSTGRES_TYPE_NOTE, SELECT_EXPRESSION_NOTE, - SELECT_EXPRESSION_TYPE_NOTE, SERIALIZE_AS_NOTE, SERIALIZE_FN_NOTE, SQLITE_TYPE_NOTE, - SQL_TYPE_NOTE, TABLE_NAME_NOTE, TREAT_NONE_AS_DEFAULT_VALUE_NOTE, TREAT_NONE_AS_NULL_NOTE, + DESERIALIZE_AS_NOTE, DESERIALIZE_FN_NOTE, MYSQL_TYPE_NOTE, POSTGRES_TYPE_NOTE, + SELECT_EXPRESSION_NOTE, SELECT_EXPRESSION_TYPE_NOTE, SERIALIZE_AS_NOTE, SERIALIZE_FN_NOTE, + SQLITE_TYPE_NOTE, SQL_TYPE_NOTE, TABLE_NAME_NOTE, TREAT_NONE_AS_DEFAULT_VALUE_NOTE, + TREAT_NONE_AS_NULL_NOTE, }; use crate::util::{parse_paren_list, CHECK_FOR_BACKEND_NOTE}; @@ -41,6 +42,7 @@ pub enum FieldAttr { SerializeAs(Ident, TypePath), DeserializeAs(Ident, TypePath), SerializeFn(Ident, Expr), + DeserializeFn(Ident, Expr), SelectExpression(Ident, Expr), SelectExpressionType(Ident, Type), } @@ -150,6 +152,10 @@ impl Parse for FieldAttr { name, parse_eq(input, SERIALIZE_FN_NOTE)?, )), + "deserialize_fn" => Ok(FieldAttr::DeserializeFn( + name, + parse_eq(input, DESERIALIZE_FN_NOTE)?, + )), "select_expression" => Ok(FieldAttr::SelectExpression( name, parse_eq(input, SELECT_EXPRESSION_NOTE)?, @@ -185,6 +191,7 @@ impl MySpanned for FieldAttr { | FieldAttr::SerializeAs(ident, _) | FieldAttr::DeserializeAs(ident, _) | FieldAttr::SerializeFn(ident, _) + | FieldAttr::DeserializeFn(ident, _) | FieldAttr::SelectExpression(ident, _) | FieldAttr::SelectExpressionType(ident, _) => ident.span(), } diff --git a/diesel_derives/src/field.rs b/diesel_derives/src/field.rs index 624d78726b03..ff4fbceb61b5 100644 --- a/diesel_derives/src/field.rs +++ b/diesel_derives/src/field.rs @@ -15,6 +15,7 @@ pub struct Field { pub serialize_as: Option>, pub deserialize_as: Option>, pub serialize_fn: Option>, + pub deserialize_fn: Option>, pub select_expression: Option>, pub select_expression_type: Option>, pub embed: Option>, @@ -31,6 +32,7 @@ impl Field { let mut serialize_as = None; let mut deserialize_as = None; let mut serialize_fn = None; + let mut deserialize_fn = None; let mut embed = None; let mut select_expression = None; let mut select_expression_type = None; @@ -90,6 +92,13 @@ impl Field { ident_span, }) } + FieldAttr::DeserializeFn(_, value) => { + deserialize_fn = Some(AttributeSpanWrapper { + item: value, + attribute_span, + ident_span, + }) + } FieldAttr::SelectExpression(_, value) => { select_expression = Some(AttributeSpanWrapper { item: value, @@ -135,6 +144,7 @@ impl Field { serialize_as, deserialize_as, serialize_fn, + deserialize_fn, select_expression, select_expression_type, embed, diff --git a/diesel_derives/src/util.rs b/diesel_derives/src/util.rs index b43ce79f4cb3..8cbbd1525284 100644 --- a/diesel_derives/src/util.rs +++ b/diesel_derives/src/util.rs @@ -11,6 +11,7 @@ pub const SQL_TYPE_NOTE: &str = "sql_type = Foo"; pub const SERIALIZE_AS_NOTE: &str = "serialize_as = Foo"; pub const DESERIALIZE_AS_NOTE: &str = "deserialize_as = Foo"; pub const SERIALIZE_FN_NOTE: &str = "serialize_fn = some_function"; +pub const DESERIALIZE_FN_NOTE: &str = "deserialize_fn = some_function"; pub const TABLE_NAME_NOTE: &str = "table_name = foo"; pub const TREAT_NONE_AS_DEFAULT_VALUE_NOTE: &str = "treat_none_as_default_value = true"; pub const TREAT_NONE_AS_NULL_NOTE: &str = "treat_none_as_null = true"; From 807e36e549b5adfdf84a8e7b1b7b1e6dcea4b091 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Fri, 27 Oct 2023 10:45:30 +0300 Subject: [PATCH 6/8] Implement `serialize_fn` also for `AsChangeset`. --- diesel_derives/src/as_changeset.rs | 50 ++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/diesel_derives/src/as_changeset.rs b/diesel_derives/src/as_changeset.rs index b4382cc5a428..5920300145e9 100644 --- a/diesel_derives/src/as_changeset.rs +++ b/diesel_derives/src/as_changeset.rs @@ -61,24 +61,33 @@ pub fn derive(item: DeriveInput) -> Result { None => treat_none_as_null, }; - match field.serialize_as.as_ref() { - Some(AttributeSpanWrapper { item: ty, .. }) => { + match (field.serialize_as.as_ref(), field.serialize_fn.as_ref()) { + (Some(AttributeSpanWrapper { item: ty, .. }), serialize_fn) => { direct_field_ty.push(field_changeset_ty_serialize_as( field, table_name, ty, treat_none_as_null, )?); - direct_field_assign.push(field_changeset_expr_serialize_as( - field, - table_name, - ty, - treat_none_as_null, - )?); + if let Some(AttributeSpanWrapper { item: function, .. }) = serialize_fn { + direct_field_ty.push(field_changeset_expr_serialize_fn( + field, + table_name, + function, + treat_none_as_null, + )?); + } else { + direct_field_assign.push(field_changeset_expr_serialize_as( + field, + table_name, + ty, + treat_none_as_null, + )?); + } generate_borrowed_changeset = false; // as soon as we hit one field with #[diesel(serialize_as)] there is no point in generating the impl of AsChangeset for borrowed structs } - None => { + (None, None) => { direct_field_ty.push(field_changeset_ty( field, table_name, @@ -104,6 +113,12 @@ pub fn derive(item: DeriveInput) -> Result { treat_none_as_null, )?); } + (None, Some(AttributeSpanWrapper { attribute_span, .. })) => { + return Err(syn::Error::new( + *attribute_span, + "`#[diesel(serialize_fn)]` requires `#[diesel(serialize_as)]` to be declared as well", + )); + } } } @@ -222,3 +237,20 @@ fn field_changeset_expr_serialize_as( Ok(quote!(#column.eq(::std::convert::Into::<#ty>::into(self.#field_name)))) } } + +fn field_changeset_expr_serialize_fn( + field: &Field, + table_name: &Path, + function: &Expr, + treat_none_as_null: bool, +) -> Result { + let field_name = &field.name; + let column_name = field.column_name()?; + column_name.valid_ident()?; + let column: Expr = parse_quote!(#table_name::#column_name); + if !treat_none_as_null && is_option_ty(&field.ty) { + Ok(quote!(self.#field_name.map(|x| #column.eq((#function)(x))))) + } else { + Ok(quote!(#column.eq((#function)(self.#field_name)))) + } +} From 75b81dd2d8c2d4aaaacb6106b0d7c4a92b47a560 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Fri, 27 Oct 2023 10:54:49 +0300 Subject: [PATCH 7/8] Add more tests for derive `Insertable` `serialize_fn`. --- diesel_derives/tests/insertable.rs | 154 ++++++++++++++++++++++++++++- 1 file changed, 153 insertions(+), 1 deletion(-) diff --git a/diesel_derives/tests/insertable.rs b/diesel_derives/tests/insertable.rs index 612ab6fc4df8..27076bc9023f 100644 --- a/diesel_derives/tests/insertable.rs +++ b/diesel_derives/tests/insertable.rs @@ -379,7 +379,7 @@ fn embedded_struct() { } #[test] -fn serialize_fn_custom_option_field() { +fn serialize_fn_custom_option_field_closure() { struct UserName(String); impl From for String { fn from(value: UserName) -> Self { @@ -390,6 +390,7 @@ fn serialize_fn_custom_option_field() { enum HairColor { Green, } + impl From for String { fn from(value: HairColor) -> Self { match value { @@ -425,3 +426,154 @@ fn serialize_fn_custom_option_field() { let expected = vec![("Sean".to_string(), Some("Green".to_string()))]; assert_eq!(Ok(expected), saved); } + +#[test] +fn serialize_fn_custom_option_field_function() { + struct UserName(String); + impl From for String { + fn from(value: UserName) -> Self { + value.0 + } + } + + enum HairColor { + Green, + } + + fn hair_color_to_string(value: &Option) -> Option { + value.map(|value| match value { + HairColor::Green => "Green".into(), + }) + } + + #[derive(Insertable)] + #[diesel(table_name = users)] + #[diesel(treat_none_as_default_value = false)] + struct NewUser { + #[diesel(serialize_as = String)] + name: UserName, + #[diesel(serialize_as = Option)] + #[diesel(serialize_fn = hair_color_to_string)] + hair_color: Option, + } + + let conn = &mut connection(); + let new_user = NewUser { + name: UserName("Sean".into()), + hair_color: Some(HairColor::Green), + }; + insert_into(users::table) + .values(new_user) + .execute(conn) + .unwrap(); + + let saved = users::table + .select((users::name, users::hair_color)) + .load::<(String, Option)>(conn); + let expected = vec![("Sean".to_string(), Some("Green".to_string()))]; + assert_eq!(Ok(expected), saved); +} + +#[test] +fn serialize_fn_custom_option_field_associated_function() { + struct UserName(String); + impl From for String { + fn from(value: UserName) -> Self { + value.0 + } + } + + enum HairColor { + Green, + } + + impl HairColor { + fn to_string(value: &Option) -> Option { + value.map(|value| match value { + HairColor::Green => "Green".into(), + }) + } + } + + #[derive(Insertable)] + #[diesel(table_name = users)] + #[diesel(treat_none_as_default_value = false)] + struct NewUser { + #[diesel(serialize_as = String)] + name: UserName, + #[diesel(serialize_as = Option)] + #[diesel(serialize_fn = HairColor::to_string)] + hair_color: Option, + } + + let conn = &mut connection(); + let new_user = NewUser { + name: UserName("Sean".into()), + hair_color: Some(HairColor::Green), + }; + insert_into(users::table) + .values(new_user) + .execute(conn) + .unwrap(); + + let saved = users::table + .select((users::name, users::hair_color)) + .load::<(String, Option)>(conn); + let expected = vec![("Sean".to_string(), Some("Green".to_string()))]; + assert_eq!(Ok(expected), saved); +} + +#[test] +fn serialize_fn_overrides_from() { + struct UserName(String); + impl From for String { + fn from(value: UserName) -> Self { + value.0 + } + } + + enum HairColor { + Green, + } + + impl From for String { + fn from(value: HairColor) -> Self { + match value { + HairColor::Green => "error".into(), + } + } + } + + fn hair_color_to_string(value: &HairColor) -> String { + match value { + HairColor::Green => "Green".into(), + } + } + + #[derive(Insertable)] + #[diesel(table_name = users)] + #[diesel(treat_none_as_default_value = false)] + struct NewUser { + #[diesel(serialize_as = String)] + name: UserName, + #[diesel(serialize_as = String)] + #[diesel(serialize_fn = hair_color_to_string)] + hair_color: HairColor, + } + + let conn = &mut connection(); + let new_user = NewUser { + name: UserName("Sean".into()), + hair_color: Some(HairColor::Green), + }; + insert_into(users::table) + .values(new_user) + .execute(conn) + .unwrap(); + + let saved = users::table + .select((users::name, users::hair_color)) + .load::<(String, Option)>(conn); + let expected = vec![("Sean".to_string(), Some("Green".to_string()))]; + assert_eq!(Ok(expected), saved); +} From ad0e4d54890b1bc0a7a3157189573c590c5c4a69 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Fri, 27 Oct 2023 11:17:07 +0300 Subject: [PATCH 8/8] Make tests compile. --- diesel_derives/tests/insertable.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/diesel_derives/tests/insertable.rs b/diesel_derives/tests/insertable.rs index 27076bc9023f..8bee5145dd26 100644 --- a/diesel_derives/tests/insertable.rs +++ b/diesel_derives/tests/insertable.rs @@ -440,7 +440,7 @@ fn serialize_fn_custom_option_field_function() { Green, } - fn hair_color_to_string(value: &Option) -> Option { + fn hair_color_to_string(value: Option) -> Option { value.map(|value| match value { HairColor::Green => "Green".into(), }) @@ -488,7 +488,7 @@ fn serialize_fn_custom_option_field_associated_function() { } impl HairColor { - fn to_string(value: &Option) -> Option { + fn to_string(value: Option) -> Option { value.map(|value| match value { HairColor::Green => "Green".into(), }) @@ -544,7 +544,7 @@ fn serialize_fn_overrides_from() { } } - fn hair_color_to_string(value: &HairColor) -> String { + fn hair_color_to_string(value: HairColor) -> String { match value { HairColor::Green => "Green".into(), } @@ -564,7 +564,7 @@ fn serialize_fn_overrides_from() { let conn = &mut connection(); let new_user = NewUser { name: UserName("Sean".into()), - hair_color: Some(HairColor::Green), + hair_color: HairColor::Green, }; insert_into(users::table) .values(new_user)