From b32095b41cfc665e302cefbab822fd31ed8e2757 Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Tue, 26 Aug 2025 13:28:43 +0400 Subject: [PATCH 1/2] From: allow specifying default values for fields When deriving `From`, it is often useful to let some fields be a certain "constant" default value instead of being part of the source tuple type. The most obvious such value is `Default::default()`, but specifying other values is very useful as well. As such, add handling of `#[from]` and `#[from()]` attrs on struct (and enum struct) fields. For more information about the handling details, please consult the included documentation changes and/or tests. Closes https://github.com/JelteF/derive_more/issues/149 --- CHANGELOG.md | 2 + impl/doc/from.md | 74 ++++++++++++++++++++++++++++++- impl/src/from.rs | 60 +++++++++++++++++++++++-- tests/from.rs | 111 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 242 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6c51ef4..b5069b10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ([#477](https://github.com/JelteF/derive_more/pull/477)) - Support `Deref` and `DerefMut` derives for enums. ([#485](https://github.com/JelteF/derive_more/pull/485)) +- Support for `#[from]` and `#[from()]` on struct fields. + ([#500](https://github.com/JelteF/derive_more/pull/500)) ### Changed diff --git a/impl/doc/from.md b/impl/doc/from.md index e025461a..1638a1d4 100644 --- a/impl/doc/from.md +++ b/impl/doc/from.md @@ -79,7 +79,78 @@ assert_eq!(Str { inner: "String".into() }, "String".to_owned().into()); assert_eq!(Str { inner: "Cow".into() }, Cow::Borrowed("Cow").to_owned().into()); ``` +Finally, for extra flexibility, you can directly specify which fields to include +in the tuple and specify defaults for the rest. NOTE: this is currently not +supported for `#[from(forward)]` or `#[from(]`; this may be alleviated in +the future. +If you add a `#[from()]` attribute to any fields of the struct, +then those fields will be omitted from the tuple and be set to the default value +in the implementation: + +```rust +# use std::collections::HashMap; +# +# use derive_more::From; +# +#[derive(Debug, From, PartialEq)] +struct MyWrapper { + inner: u8, + #[from(1)] + not_important: u32, + #[from(HashMap::new())] + extra_properties: HashMap, +} + +assert_eq!(MyWrapper { inner: 123, not_important: 1, extra_properties: HashMap::new(), }, 123.into()); +``` + + +If you add a `#[from]` value to any fields of the struct, then only those +fields will be present in the tuple and the rest will be either set to +`Default::default()` or taken from their default values specified in +`#[from()]`: + +```rust + +# use std::collections::HashMap; +# +# use derive_more::From; +# +#[derive(Debug, From, PartialEq)] +struct Location { + #[from] + lat: f32, + #[from] + lon: f32, + #[from(String::from("Check out my location!"))] + description: String, + extra_properties: HashMap, +} + +// This is equivalent to: + +// #[derive(Debug, From, PartialEq)] +// struct Location { +// lat: f32, +// lon: f32, +// #[from(String::from("Check out my location!"))] +// description: String, +// #[from(Default::default())] +// extra_properties: HashMap, +// } + + +assert_eq!( + Location { + lat: 41.7310, + lon: 44.8067, + description: String::from("Check out my location!"), + extra_properties: Default::default(), + }, + (41.7310, 44.8067).into() +); +``` ## Enums @@ -132,7 +203,8 @@ enum Int { ``` - +`#[from]`/`#[from()]` may also be used on fields of enum variants +in the same way as for struct fields. ## Example usage diff --git a/impl/src/from.rs b/impl/src/from.rs index a345cab0..71bffd24 100644 --- a/impl/src/from.rs +++ b/impl/src/from.rs @@ -148,8 +148,31 @@ impl Expansion<'_> { fn expand(&self) -> syn::Result { use crate::utils::FieldsExt as _; + let attr_name = format_ident!("from"); let ident = self.ident; - let field_tys = self.fields.iter().map(|f| &f.ty).collect::>(); + let fields_explicit_from: Vec<&syn::Type> = self + .fields + .iter() + .filter(|field| { + field.attrs.iter().any(|attr| match attr.meta.clone() { + syn::Meta::Path(path) => path.is_ident(&attr_name), + _ => false, + }) + }) + .map(|f| &f.ty) + .collect(); + let has_explicit_from_fields = !fields_explicit_from.is_empty(); + let field_tys = if has_explicit_from_fields { + fields_explicit_from + } else { + self.fields + .iter() + .filter(|f| { + !f.attrs.iter().any(|attr| attr.path().is_ident(&attr_name)) + }) + .map(|f| &f.ty) + .collect::>() + }; let (impl_gens, ty_gens, where_clause) = self.generics.split_for_impl(); let skip_variant = self.has_explicit_from @@ -188,10 +211,39 @@ impl Expansion<'_> { } (Some(VariantAttribute::Empty(_)), _) | (None, false) => { let variant = self.variant.iter(); - let init = self.expand_fields(|ident, _, index| { + let mut fields = self.fields.iter(); + let mut index: Option = + if field_tys.len() > 1 { Some(0) } else { None }; + let init = self.expand_fields(|ident, _, _| { let ident = ident.into_iter(); - let index = index.into_iter(); - quote! { #( #ident: )* value #( . #index )*, } + let field = fields.next().unwrap(); + + let from_val = if let Some(attr) = field + .attrs + .iter() + .find(|a| a.meta.path().is_ident(&format_ident!("from"))) + { + match attr.meta.clone() { + syn::Meta::List(meta_list) => Some(meta_list.tokens), + syn::Meta::Path(_) => None, + _ => panic!("Only #[from] and #[from()] are supported"), + } + } else if has_explicit_from_fields { + Some(quote! { Default::default() }) + } else { + None + }; + if let Some(value) = from_val { + quote! { + #( #ident: )* #value, + } + } else { + let index_ = index.into_iter().map(syn::Index::from); + index = index.map(|v| v + 1); + quote! { + #( #ident: )* value #( . #index_ )*, + } + } }); Ok(quote! { diff --git a/tests/from.rs b/tests/from.rs index 0fcb0b31..4d5a9039 100644 --- a/tests/from.rs +++ b/tests/from.rs @@ -290,6 +290,85 @@ mod structs { } } + mod default_fields { + use super::*; + #[derive(Debug, From, PartialEq)] + struct StructAllDefaultsApartFromOne { + field1: i32, + #[from(1)] + field2_has_default: i16, + #[from(Default::default())] + field3_has_default: bool, + #[from(Default::default())] + field4_has_default: Option, + } + #[derive(Debug, From, PartialEq)] + struct StructAllDefaultsApartFromTwo { + field1: i32, + #[from(1)] + field2_has_default: i16, + field3: bool, + #[from(Default::default())] + field4_has_default: Option, + } + #[derive(Debug, From, PartialEq)] + struct StructImplicitDefaults { + #[from] + field1: i32, + field2_implicit_default: bool, + field3_implicit_default: Option, + } + #[derive(Debug, From, PartialEq)] + struct StructImplicitAndExplicitDefaults { + #[from] + field1: i32, + #[from] + field2: i16, + #[from(true)] + field3_has_default: bool, + field4_implicit_default: Option, + } + + #[test] + fn assert() { + assert_eq!( + StructAllDefaultsApartFromOne { + field1: 123, + field2_has_default: 1, + field3_has_default: false, + field4_has_default: None, + }, + 123.into(), + ); + assert_eq!( + StructAllDefaultsApartFromTwo { + field1: 123, + field2_has_default: 1, + field3: true, + field4_has_default: None, + }, + (123, true).into(), + ); + assert_eq!( + StructImplicitDefaults { + field1: 123, + field2_implicit_default: false, + field3_implicit_default: None, + }, + 123.into(), + ); + assert_eq!( + StructImplicitAndExplicitDefaults { + field1: 123, + field2: 1, + field3_has_default: true, + field4_implicit_default: None, + }, + (123, 1).into(), + ); + } + } + mod forward { use super::*; @@ -579,11 +658,43 @@ mod enums { AutomaticallySkipped {}, } + #[derive(Debug, From, PartialEq)] + enum DefaultFields { + #[from] + Variant1 { + #[from] + field1: i32, + field2: bool, + }, + #[from] + Variant2 { + #[from] + field1: String, + #[from(String::from("This should be field2"))] + field2: String, + }, + AutomaticallySkipped, + } + #[test] fn assert() { assert_eq!(Unit::Variant, ().into()); assert_eq!(Tuple::Variant(), ().into()); assert_eq!(Struct::Variant {}, ().into()); + assert_eq!( + DefaultFields::Variant1 { + field1: 123, + field2: false, + }, + 123.into(), + ); + assert_eq!( + DefaultFields::Variant2 { + field1: String::from("This should be field1"), + field2: String::from("This should be field2"), + }, + String::from("This should be field1").into(), + ); } mod r#const { From ffa1cb34376d9f752355516991eb1364915cd4ac Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Tue, 26 Aug 2025 13:51:54 +0400 Subject: [PATCH 2/2] Fix a non-working (and non-failing) test My changes have broken a test, which actually was actually incorrect twice: it wasn't doing the right thing (forwarding the From impl to the field type), but it wasn't testing the right thing either so it succeeded. Make the test actually check that from forwarding works, and make the forwarding actually work. --- tests/lib.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/lib.rs b/tests/lib.rs index 4899ae56..6612e6f7 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -169,10 +169,17 @@ struct Unit; // containing `$crate` macro_rules! use_dollar_crate { () => { - struct Foo; - #[derive(From)] + #[derive(From, Debug, PartialEq)] + struct Foo(u32); + #[derive(From, Debug, PartialEq)] enum Bar { - First(#[from(forward)] $crate::Foo), + #[from(forward)] + First($crate::Foo), + } + + #[test] + fn test_dollar_crate() { + assert_eq!(Bar::First(Foo(123)), 123.into()); } }; }