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 { 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()); } }; }