From ad27648f3a2121cbb0f7947f8dc69ab70464e84f Mon Sep 17 00:00:00 2001 From: Anatoly Ikorsky Date: Mon, 1 Jan 2024 22:00:58 +0300 Subject: [PATCH] Add `with` field attribute for the FromRow derive macro --- derive/src/from_row/structs/attrs/field.rs | 6 +- derive/src/from_row/structs/mod.rs | 15 +++- src/lib.rs | 80 ++++++++++++++++++++++ 3 files changed, 98 insertions(+), 3 deletions(-) diff --git a/derive/src/from_row/structs/attrs/field.rs b/derive/src/from_row/structs/attrs/field.rs index ca351b2..1851ddf 100644 --- a/derive/src/from_row/structs/attrs/field.rs +++ b/derive/src/from_row/structs/attrs/field.rs @@ -1,4 +1,6 @@ -use darling::FromAttributes; +use darling::{util::SpannedValue, FromAttributes}; + +use crate::from_value::structs::attrs::container::FnPath; #[derive(Debug, Default, FromAttributes)] #[darling(attributes(mysql))] @@ -7,4 +9,6 @@ pub struct Mysql { pub json: bool, #[darling(default)] pub rename: Option, + #[darling(default)] + pub with: Option>, } diff --git a/derive/src/from_row/structs/mod.rs b/derive/src/from_row/structs/mod.rs index 91047e9..d30ff8e 100644 --- a/derive/src/from_row/structs/mod.rs +++ b/derive/src/from_row/structs/mod.rs @@ -175,10 +175,17 @@ impl ToTokens for GenericStruct<'_> { quote::quote!(<#ty as FromValue>::Intermediate) }; + let try_from = if let Some(ref path) = attrs.with { + let path = &path.0; + quote::quote!( #path(x) ) + } else { + quote::quote!( <#intermediate_ty as std::convert::TryFrom>::try_from(x) ) + }; + quote::quote!( let #ident = { let val = match row.take_opt::(#lit) { - Some(Ok(x)) => match <#intermediate_ty as std::convert::TryFrom>::try_from(x) { + Some(Ok(x)) => match #try_from { Ok(x) => Some(x), Err(e) => { row.place(*indexes.get(#lit).unwrap(), e.0); @@ -210,7 +217,11 @@ impl ToTokens for GenericStruct<'_> { if attrs.json { quote::quote!(#ident: #ident.commit().0) } else { - quote::quote!(#ident: <<#ty as FromValue>::Intermediate as std::convert::Into<#ty>>::into(#ident)) + if attrs.with.is_some() { + quote::quote!( #ident ) + } else { + quote::quote!(#ident: <<#ty as FromValue>::Intermediate as std::convert::Into<#ty>>::into(#ident)) + } } }) .collect::>(); diff --git a/src/lib.rs b/src/lib.rs index 7d9b74a..8b45168 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -261,6 +261,9 @@ //! * `#[mysql(rename = "some_name")]` – overrides column name of a field //! * `#[mysql(json)]` - column will be interpreted as a JSON string containing //! a value of a field type +//! * `#[mysql(with = path::to::convert_fn)]` – `convert_fn` will be used to deserialize +//! a field value (expects a function with a signature that mimics +//! `TryFrom``) //! //! ### Example //! @@ -647,3 +650,80 @@ fn from_value_is_integer() { assert_eq!(Value::from(SomeTypeIsInteger::SecondVariant), Value::Int(2)); assert_eq!(Value::from(SomeTypeIsInteger::ThirdVariant), Value::Int(3)); } + +#[cfg(test)] +mod tests { + use crate::row::convert::FromRow; + use crate::{constants::ColumnType, packets::Column, row::new_row}; + + #[test] + fn from_row_derive() { + #[derive(FromRow)] + #[mysql(table_name = "Foos", rename_all = "camelCase")] + struct Foo { + id: u64, + text_data: String, + #[mysql(json)] + json_data: serde_json::Value, + #[mysql(with = "from_literal", rename = "custom")] + custom_bool: bool, + } + + fn from_literal(value: crate::Value) -> Result { + match value { + crate::Value::Bytes(x) if x == b"true" => Ok(true), + crate::Value::Bytes(x) if x == b"false" => Ok(false), + x => Err(crate::FromValueError(x)), + } + } + + assert_eq!(Foo::TABLE_NAME, "Foos"); + assert_eq!(Foo::ID_FIELD, "id"); + assert_eq!(Foo::TEXT_DATA_FIELD, "textData"); + assert_eq!(Foo::JSON_DATA_FIELD, "jsonData"); + assert_eq!(Foo::CUSTOM_BOOL_FIELD, "custom"); + + let columns = vec![ + Column::new(ColumnType::MYSQL_TYPE_LONGLONG) + .with_name(b"id") + .with_org_name(b"id") + .with_table(b"Foos") + .with_org_table(b"Foos"), + Column::new(ColumnType::MYSQL_TYPE_VARCHAR) + .with_name(b"textData") + .with_org_name(b"textData") + .with_table(b"Foos") + .with_org_table(b"Foos"), + Column::new(ColumnType::MYSQL_TYPE_JSON) + .with_name(b"jsonData") + .with_org_name(b"jsonData") + .with_table(b"Foos") + .with_org_table(b"Foos"), + Column::new(ColumnType::MYSQL_TYPE_VARCHAR) + .with_name(b"custom") + .with_org_name(b"custom") + .with_table(b"Foos") + .with_org_table(b"Foos"), + ]; + + let row = new_row( + vec![ + crate::Value::Int(10), + crate::Value::Bytes(b"bytes".into()), + crate::Value::Bytes(b"[true,false,\"not found\"]".into()), + crate::Value::Bytes(b"true".into()), + ], + columns.into(), + ); + + let deserialized = Foo::from_row(row); + + assert_eq!(deserialized.id, 10); + assert_eq!(deserialized.text_data, "bytes"); + assert_eq!( + deserialized.json_data.to_string(), + "[true,false,\"not found\"]" + ); + assert!(deserialized.custom_bool); + } +}