From e43671da3c2042d04b5c10359ca66de3c9c6c515 Mon Sep 17 00:00:00 2001 From: ImaMapleTree <59880284+ImaMapleTree@users.noreply.github.com> Date: Thu, 30 May 2024 18:55:18 +0900 Subject: [PATCH] - added support for #[ctor(Default)] on structs which auto-implements the `Default` trait - bumped version to 0.2.1 - cleaned up README.md - fixed no-std feature --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 44 +++++++++++++---- src/enums.rs | 17 +++++-- src/fields.rs | 9 ++-- src/lib.rs | 8 ++++ src/structs.rs | 91 +++++++++++++++++++++++++++++------- tests/enum_variant_config.rs | 4 +- tests/struct_ctor_config.rs | 38 +++++++++++++++ tests/struct_field_all.rs | 5 ++ 10 files changed, 183 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 59dea4b..d132998 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 3 [[package]] name = "derive-ctor" -version = "0.2.0" +version = "0.2.1" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 41fa84b..ab2db0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "derive-ctor" -version = "0.2.0" +version = "0.2.1" description = "Adds `#[derive(ctor)]` which allows for the auto-generation of a constructor." keywords = ["derive", "macro", "trait", "procedural", "no_std"] authors = ["Evan Cowin"] diff --git a/README.md b/README.md index e4cee97..a6611f2 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ - Customize the name and visibility of the auto-generated constructor using `#[ctor(visibility method_name)]`. - Supports const constructors by adding the "const" keyword. - Provide a list of names to generate multiple constructors. -- Customize field behavior in the constructor with the following attributes: - - `#[ctor(cloned)]` - Changes the parameter type to accept a reference type which is then cloned into the created struct. - - `#[ctor(default)]` - Exclude the field from the generated method and use its default value. - - `#[ctor(expr(EXPRESSION))]` - Exclude the field from the generated method and use the defined expression as its default value. - - Use `#[ctor(expr!(EXPRESSION))]` to add the annotated field as a required parameter, allowing the expression to reference itself. - - Use `#[ctor(expr(TYPE -> EXPRESSION))]` to add a parameter with the specified type, which will be used to generate the final field value. - - `#[ctor(into)]` - Change the parameter type for the generated method to `impl Into`. - - `#[ctor(iter(FROM_TYPE))]` - Change the parameter type for the generated method to `impl IntoIterator`. +- Customize field behavior in the constructor with the following properties (used in `#[ctor(PROPETY)])`: + - **cloned** - Changes the parameter type to accept a reference type which is then cloned into the created struct. + - **default** - Exclude the field from the generated method and use its default value. + - **expr(EXPRESSION)** - Exclude the field from the generated method and use the defined expression as its default value. + - **expr!(EXPRESSION)** to add the annotated field as a required parameter, allowing the expression to reference itself. + - Use **expr(TYPE -> EXPRESSION)** to add a parameter with the specified type, which will be used to generate the final field value. + - **into** - Change the parameter type for the generated method to `impl Into`. + - **iter(FROM_TYPE)** - Change the parameter type for the generated method to `impl IntoIterator`. - Support no-std via `features = ["no-std"]` ## Basic Usage @@ -24,7 +24,7 @@ Add `derive-ctor` to your `Cargo.toml`: ```toml [dependencies] -derive-ctor = "0.1.1" +derive-ctor = "0.2.1" ``` Import the crate in your Rust code: @@ -48,6 +48,8 @@ let my_struct = MyStruct::new(1, String::from("Foo")); ## Struct Configurations +### Visibiltiy and Construtor Name + You can modify the name and visibility of the generated method, and define additional constructors by using the `#[ctor]` attribute on the target struct after `ctor` is derived. @@ -58,11 +60,33 @@ These methods all inherit their respective visibilities defined within the `#[ct use derive_ctor::ctor; #[derive(ctor)] -#[ctor(pub new, pub(crate) with_defaults, const internal)] +#[ctor(pub new, pub(crate) other, const internal)] struct MyStruct { field1: i32, field2: String } + +let my_struct1 = MyStruct::new(100, "A".to_string()); +let my_struct2 = MyStruct::other(200, "B".to_string()); +let my_struct3 = MyStruct::internal(300, "C".to_string()); +``` + +### Auto-implement "Default" Trait +The `Default` trait can be auto implemented by specifying a ctor with the name "Default" in the ctor attribute. Note: all fields must have a generated value in order for the implementation to be valid. + +```rust +use derive_ctor::ctor; + +#[derive(ctor)] +#[ctor(Default)] +struct MyStruct { + #[ctor(default)] + field1: i32, + #[ctor(expr(true))] + field2: bool +} + +let default: MyStruct = Default::default(); ``` ## Enum Configurations diff --git a/src/enums.rs b/src/enums.rs index 03847ee..a45ee32 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -1,13 +1,22 @@ use proc_macro::TokenStream; +#[cfg(feature = "no-std")] +use alloc::string::String; +#[cfg(feature = "no-std")] +use alloc::vec; +#[cfg(feature = "no-std")] +use alloc::string::ToString; +#[cfg(feature = "no-std")] +use alloc::vec::Vec; + use proc_macro2::Span; -use quote::{quote}; +use quote::quote; use syn::{Data, DeriveInput, Error, Fields, Generics, Ident, token, Variant, Visibility}; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::token::{Comma, Pub}; use crate::{CONFIG_PROP_ERR_MSG, try_parse_attributes_with_default}; -use crate::structs::{CtorDefinition, CtorStructConfiguration, generate_ctor_meta_from_fields}; +use crate::structs::{generate_ctor_meta_from_fields, CtorAttribute, CtorDefinition, CtorStructConfiguration}; static ENUM_CTOR_PROPS: &str = "\"prefix\", \"visibility\", \"vis\""; @@ -35,7 +44,7 @@ impl CtorStructConfiguration { None => variant_name, Some(prefix) => syn::parse_str(&(prefix.to_string() + "_" + &variant_name.to_string())).unwrap() }, - is_const: false, + attributes: Default::default(), }], is_none: false } } } @@ -137,7 +146,7 @@ fn create_ctor_enum_impl( Err(err) => return TokenStream::from(err.to_compile_error()) }; - let const_tkn = if definition.is_const { quote! { const } } else { quote!{} }; + let const_tkn = if definition.attributes.contains(&CtorAttribute::Const) { quote! { const } } else { quote!{} }; let enum_generation = if variant_code == 0 { quote! { Self::#variant_name { #(#field_idents),* } } diff --git a/src/fields.rs b/src/fields.rs index 273ebc6..af17cd8 100644 --- a/src/fields.rs +++ b/src/fields.rs @@ -9,7 +9,7 @@ use alloc::string::ToString; use std::collections::HashSet; -use proc_macro2::{Delimiter, Punct}; +use proc_macro2::{Delimiter, Punct, Span}; use proc_macro2::Spacing::Alone; use quote::{quote, TokenStreamExt, ToTokens}; use syn::{Error, Ident, LitInt, token, Type, Token}; @@ -54,13 +54,16 @@ pub(crate) enum FieldConfigProperty { #[derive(Clone)] pub(crate) struct ParameterField { pub(crate) field_ident: Ident, - pub(crate) field_type: Type + pub(crate) field_type: Type, + pub(crate) span: Span } #[derive(Clone)] pub(crate) struct GeneratedField { pub(crate) field_ident: Ident, - pub(crate) configuration: FieldConfigProperty + pub(crate) configuration: FieldConfigProperty, + #[allow(dead_code /*may be used for future purposes*/)] + pub(crate) span: Span } impl Parse for FieldConfig { diff --git a/src/lib.rs b/src/lib.rs index eae64b9..900f844 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,14 @@ #![cfg_attr(feature = "no-std", no_std, doc = "Removes all std library dependencies within library.")] #![doc = include_str!("../README.md")] +#[cfg(feature = "no-std")] +extern crate alloc; +#[cfg(feature = "no-std")] +use alloc::format; +#[cfg(feature = "no-std")] +use alloc::string::ToString; + + use proc_macro::TokenStream; use proc_macro2::Delimiter; use quote::ToTokens; diff --git a/src/structs.rs b/src/structs.rs index b119474..2539555 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -5,7 +5,12 @@ use alloc::vec; #[cfg(feature = "no-std")] use alloc::vec::Vec; #[cfg(feature = "no-std")] +use alloc::collections::BTreeSet as HashSet; +#[cfg(feature = "no-std")] +use alloc::string::ToString; +#[cfg(not(feature = "no-std"))] +use std::collections::HashSet; #[cfg(not(feature = "no-std"))] use std::vec; #[cfg(not(feature = "no-std"))] @@ -15,6 +20,7 @@ use proc_macro::TokenStream; use proc_macro2::Span; use quote::quote; +use syn::spanned::Spanned; use syn::{parse2, Data, DeriveInput, Error, Fields, Generics, Ident, Visibility}; use syn::parse::{ParseStream, Parse}; use syn::token::{Comma, Const, Pub}; @@ -22,10 +28,18 @@ use syn::token::{Comma, Const, Pub}; use crate::fields::{FieldConfig, FieldConfigProperty, GeneratedField, ParameterField}; use crate::{is_phantom_data, try_parse_attributes, try_parse_attributes_with_default}; +static DEFAULT_CTOR_ERR_MSG: &'static str = "Default constructor requires field to generate its own value."; + pub(crate) struct CtorDefinition { pub(crate) visibility: Visibility, pub(crate) ident: Ident, - pub(crate) is_const: bool + pub(crate) attributes: HashSet +} + +#[derive(Hash, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) enum CtorAttribute { + Const, + Default } impl Default for CtorDefinition { @@ -33,7 +47,7 @@ impl Default for CtorDefinition { Self { visibility: Visibility::Public(Pub { span: Span::call_site() }), ident: Ident::new("new", Span::mixed_site()), - is_const: false + attributes: Default::default() } } } @@ -58,28 +72,38 @@ impl Parse for CtorStructConfiguration { let mut definitions = Vec::new(); loop { - let mut is_const = input.parse::().is_ok(); + let mut attributes = HashSet::new(); + if input.parse::().is_ok() { + attributes.insert(CtorAttribute::Const); + } let definition = if !input.peek(syn::Ident) { let visibility = input.parse()?; - is_const = input.parse::().is_ok() || is_const; // required to support both: VIS const and const VIS + // required to support both: VIS const and const VIS + if input.parse::().is_ok() { + attributes.insert(CtorAttribute::Const); + } CtorDefinition { visibility, ident: input.parse()?, - is_const + attributes } } else { let ident = input.parse::()?; - // check for "none" as first parameter, if exists return early (this is only applicable for enums) - if definitions.is_empty() && ident.to_string() == "none" { - return Ok(CtorStructConfiguration { definitions: Default::default(), is_none: true }) + match ident.to_string().as_str() { + // check for "none" as first parameter, if exists return early (this is only applicable for enums) + "none" if definitions.is_empty() => { + return Ok(CtorStructConfiguration { definitions: Default::default(), is_none: true }) + }, + "Default" => { attributes.insert(CtorAttribute::Default); }, + _ => {} } CtorDefinition { visibility: Visibility::Inherited, ident, - is_const + attributes } }; @@ -128,27 +152,56 @@ fn create_ctor_struct_impl( let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let mut methods = Vec::new(); + let mut default_method = None; for (i, definition) in configuration.definitions.into_iter().enumerate() { let method_req_fields = &meta.parameter_fields[i]; let method_gen_fields = &meta.generated_fields[i]; let visibility = definition.visibility; - let name = definition.ident; - let const_tkn = if definition.is_const { quote! { const } } else { quote!{} }; + let mut name = definition.ident; + let const_tkn = if definition.attributes.contains(&CtorAttribute::Const) + { quote! { const } } else { quote!{} }; + + let is_default = definition.attributes.contains(&CtorAttribute::Default); + if is_default { + name = syn::parse_str("default").unwrap(); + } - methods.push(quote! { + let method_token_stream = quote! { #visibility #const_tkn fn #name(#(#method_req_fields),*) -> Self { #(#method_gen_fields)* Self { #(#field_idents),* } } - }) + }; + + if is_default { + if !method_req_fields.is_empty() { + let first_error = Error::new(method_req_fields[0].span, DEFAULT_CTOR_ERR_MSG); + let errors = method_req_fields.into_iter().skip(1).fold(first_error, |mut e, f| { + e.combine(Error::new(f.span, DEFAULT_CTOR_ERR_MSG)); + e + }); + return TokenStream::from(errors.to_compile_error()) + } + default_method = Some(method_token_stream); + } else { + methods.push(method_token_stream); + } } + let default_impl = if let Some(def_method) = default_method { + quote! { + impl #impl_generics Default for # ident # ty_generics #where_clause { + #def_method + } + } + } else { quote!{} }; TokenStream::from(quote! { impl #impl_generics #ident #ty_generics #where_clause { #(#methods)* } + #default_impl }) } @@ -174,12 +227,15 @@ pub(crate) fn generate_ctor_meta_from_fields(fields: Fields, method_count: usize for (field_index, field) in fields.into_iter().enumerate() { let configuration = try_parse_attributes::(&field.attrs)?; + let span = field.span(); + let field_ident = field.ident.unwrap_or_else(|| { - Ident::new(&("arg".to_owned() + &field_index.to_string()), Span::mixed_site()) + Ident::new(&("arg".to_string() + &field_index.to_string()), Span::mixed_site()) }); meta.field_idents.push(field_ident.clone()); + for i in 0..method_count { let field_ident = field_ident.clone(); @@ -214,14 +270,17 @@ pub(crate) fn generate_ctor_meta_from_fields(fields: Fields, method_count: usize } } + + if let Some(cfg) = gen_configuration { meta.generated_fields[i].push(GeneratedField { field_ident: field_ident.clone(), - configuration: cfg + configuration: cfg, + span }) } if let Some(field_type) = req_field_type { - meta.parameter_fields[i].push(ParameterField { field_ident, field_type }) + meta.parameter_fields[i].push(ParameterField { field_ident, field_type, span }) } } } diff --git a/tests/enum_variant_config.rs b/tests/enum_variant_config.rs index bf73d57..7b2a487 100644 --- a/tests/enum_variant_config.rs +++ b/tests/enum_variant_config.rs @@ -18,7 +18,7 @@ fn test_variant_with_configured_ctors() { #[derive(ctor, Debug, PartialEq)] enum EnumNoVariantGeneration { Variant1, - #[ctor(none)] + #[ctor(none)] #[allow(dead_code)] Variant2 } @@ -34,7 +34,7 @@ enum VariantConfigOverridesDefaults { Variant1, #[ctor(variant2)] Variant2, - #[ctor(none)] + #[ctor(none)] #[allow(dead_code)] Variant3 } diff --git a/tests/struct_ctor_config.rs b/tests/struct_ctor_config.rs index c431145..f5307db 100644 --- a/tests/struct_ctor_config.rs +++ b/tests/struct_ctor_config.rs @@ -55,4 +55,42 @@ struct FieldStructCustomCtor { fn test_field_struct_with_custom_ctor_name() { let field_struct = FieldStructCustomCtor::init(15); assert_eq!(FieldStructCustomCtor { value: 15 }, field_struct); +} + +#[derive(Debug, PartialEq)] +struct NoDefault { + +} + +#[derive(ctor, Debug, PartialEq)] +#[ctor(Default)] +struct DefaultCtorStruct { + #[ctor(expr(NoDefault {}))] + name: NoDefault, + #[ctor(default)] + value: i32 +} + +#[test] +fn test_struct_with_default_ctor() { + let result = Default::default(); + assert_eq!(DefaultCtorStruct { name: NoDefault {}, value: 0 }, result); +} + +#[derive(ctor, Debug, PartialEq)] +#[ctor(pub new, Default)] +struct TestDefaultCtorWithTargetedFieldConfig { + #[ctor(expr(String::from("Default")) = 1)] + name: String, + #[ctor(expr(404) = 1)] + value: u32 +} + +#[test] +fn test_struct_with_targeted_field_default_ctor() { + let non_default = TestDefaultCtorWithTargetedFieldConfig::new(String::from("Foo"), 505); + assert_eq!(TestDefaultCtorWithTargetedFieldConfig { name: String::from("Foo"), value: 505 }, non_default); + + let default = Default::default(); + assert_eq!(TestDefaultCtorWithTargetedFieldConfig { name: String::from("Default"), value: 404}, default); } \ No newline at end of file diff --git a/tests/struct_field_all.rs b/tests/struct_field_all.rs index 0737762..09cab92 100644 --- a/tests/struct_field_all.rs +++ b/tests/struct_field_all.rs @@ -1,3 +1,8 @@ +#![no_std] + +extern crate alloc; + +use alloc::string::String; use derive_ctor::ctor; #[derive(ctor, Debug, PartialEq)]