diff --git a/Cargo.lock b/Cargo.lock index ac0bae4..59dea4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 3 [[package]] name = "derive-ctor" -version = "0.1.1" +version = "0.2.0" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index bab2c8f..41fa84b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "derive-ctor" -version = "0.1.1" +version = "0.2.0" 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 02d9ad8..e4cee97 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,21 @@ # derive-ctor -`derive-ctor` is a Rust procedural macro crate that allows you to easily generate constructor methods for your structs. With the `#[derive(ctor)]` attribute, you can automatically create a constructor for all fields in the struct. The crate also provides various options to customize the generated constructor methods. +`derive-ctor` is a Rust procedural macro crate that allows you to easily generate constructor methods for your structs. With the `#[derive(ctor)]` attribute, you can automatically create a constructor(s) for structs and enums. The crate also provides various options to customize the generated constructor methods. ## Features -- Automatically generate a constructor method for all fields in a struct with `#[derive(ctor)]`. +- Automatically generate a constructor method for structs and enums with `#[derive(ctor)]`. - Customize the name and visibility of the auto-generated constructor using `#[ctor(visibility method_name)]`. -- Provide a list of names to generate multiple constructors. + - 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. - - `#[ctor(impl)]` - Change the parameter type for the generated method to `impl Into`. + - 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`. - Support no-std via `features = ["no-std"]` ## Basic Usage @@ -41,7 +46,7 @@ struct MyStruct { let my_struct = MyStruct::new(1, String::from("Foo")); ``` -## Configurations +## Struct Configurations 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. @@ -53,18 +58,84 @@ These methods all inherit their respective visibilities defined within the `#[ct use derive_ctor::ctor; #[derive(ctor)] -#[ctor(pub new, pub(crate) with_defaults, internal)] +#[ctor(pub new, pub(crate) with_defaults, const internal)] struct MyStruct { field1: i32, field2: String } ``` -### Field Configurations +## Enum Configurations + +By default a constructor will be generated for each variant. This constructor by default will match the name of its +respective variant and will be public. This default behaviour can be changed by annotating the enum with +`#[ctor(prefix = PREFIX, visibility = VISIBILITY)]`. Note that both parameters are optional within the attribute. +Specifying this attribute will change the **default** generated method for each variant, however, each variant +can additionally define its own configuration which overrides the one defined by the enum. + +### Default variant constructor example + +```rust +use derive_ctor::ctor; + +#[derive(ctor)] +enum MyEnum { + Variant1, + Variant2(i32), + Variant3 { value: bool } +} + +let v1 = MyEnum::variant1(); +let v2 = MyEnum::variant2(100); +let v3 = MyEnum::variant3(true); +``` + +### Configured variant constructor example +Variant constructor configuration is identical to struct constructor configuration. Refer to the below for +sample syntax or go-back to the struct constructor configuration for more information. + +```rust +use derive_ctor::ctor; + +#[derive(ctor)] +#[ctor(prefix = new, vis = pub(crate))] +enum MyEnum { + #[ctor(const pub v1, other)] + Variant1, + Variant2, + Variant3 +} + +const v1_1: MyEnum = MyEnum::v1(); +let v1_2 = MyEnum::other(); +let v2 = MyEnum::new_variant2(); +let v3 = MyEnum::new_variant3(); +``` + +If a variant is derived with `#[ctor(none)]` it will **not** have a constructor generated for it. + +## Field Configurations Fields can also be annotated with `#[ctor(PROPERTY)]` to change their behaviour in the generated methods. +**These configurations work for ALL enum-types and structs!** The following are the available properties that can be used with the field-attributes +`#[ctor(cloned)]` - This property creates a parameter that accepts a type reference of the annotated field and +then clones it to generate the final value. +```rust +use derive_ctor::ctor; + +#[derive(ctor)] +struct MyStruct { + field1: i32, + #[ctor(cloned)] + field2: String +} + +let string = String::from("Foo"); +let my_struct = MyStruct::new(100, &string); +``` + `#[ctor(default)]` - This property excludes the annotated field from the constructor and uses its default value. ```rust use derive_ctor::ctor; @@ -79,7 +150,7 @@ struct MyStruct { let my_struct = MyStruct::new(100); ``` -`#[ctor(impl)]` - This property modifies the parameter type of the annotated field for the generated method +`#[ctor(into)]` - This property modifies the parameter type of the annotated field for the generated method converting it from `Type` -> `impl Into`. ```rust use derive_ctor::ctor; @@ -87,15 +158,24 @@ use derive_ctor::ctor; #[derive(ctor)] struct MyStruct { field1: i32, - #[ctor(impl)] // the parameter type will now be impl Into instead of String + #[ctor(into)] // the parameter type will now be impl Into instead of String field2: String } let my_struct = MyStruct::new(100, "Foo"); ``` -`#[ctor(expr(VALUE))]` - This property excludes the annotated field from the constructor and utilizes the defined expression +`#[ctor(expr(EXPRESSION))]` - This property excludes the annotated field from the constructor and utilizes the defined expression to generate its value. + +**Alternatives:** + +- `#[ctor(expr!(EXPRESSION))]` - Unlike the above attribute, this attribute will add the annotated field as a required parameter +for the given constructor, this allows for the provided EXPRESSION to reference the parameter and modify the passed value. +- `#[ctor(expr(TYPE -> EXPRESSION))]` - This attribute behaves similar to the variation above, however, the required parameter +type will be of the type provided in the attribute, thus allowing for a constructor to accept and map a parameter from one type +to the type used by the struct field. + ```rust use derive_ctor::ctor; @@ -103,10 +183,31 @@ use derive_ctor::ctor; struct MyStruct { field1: i32, #[ctor(expr(String::from("Foo")))] - field2: String + field2: String, + #[ctor(expr!(field3 + 100))] + field3: u32, + #[ctor(expr(i32 -> field4 < 0))] + field4: bool +} + +let my_struct = MyStruct::new(100, 5, -20); // generates MyStruct { field1: 100, field2: "foo", field3: 105, field4: true } +``` + +`#[ctor(iter(TYPE))]` - This property adds a parameter with the type: `impl IntoIterator` and then generates +the annotated struct value by calling `.into_iter().collect()` on the parameter value. + +```rust +use std::collections::HashSet; +use derive_ctor::ctor; + +#[derive(ctor)] +struct MyStruct { + field1: i32, + #[ctor(iter(usize))] + field2: HashSet } -let my_struct = MyStruct::new(100); // generates MyStruct { field1: 100, field2: "foo" } +let my_struct = MyStruct::new(0, vec![1, 1, 2, 3, 4]); ``` ### Advanced Configuration diff --git a/src/enums.rs b/src/enums.rs new file mode 100644 index 0000000..03847ee --- /dev/null +++ b/src/enums.rs @@ -0,0 +1,214 @@ +use proc_macro::TokenStream; + +use proc_macro2::Span; +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}; + +static ENUM_CTOR_PROPS: &str = "\"prefix\", \"visibility\", \"vis\""; + +enum EnumConfigItem { + Visibility { visibility: Visibility }, + Prefix { prefix: Ident } +} + +struct CtorEnumConfiguration { + prefix: Option, + default_visibility: Visibility +} + +impl Default for CtorEnumConfiguration { + fn default() -> Self { + Self { prefix: None, default_visibility: Visibility::Public(Pub { span: Span::mixed_site() }) } + } +} + +impl CtorStructConfiguration { + fn from_variant(configuration: &CtorEnumConfiguration, variant_name: Ident) -> Self { + Self { definitions: vec![CtorDefinition { + visibility: configuration.default_visibility.clone(), + ident: match &configuration.prefix { + None => variant_name, + Some(prefix) => syn::parse_str(&(prefix.to_string() + "_" + &variant_name.to_string())).unwrap() + }, + is_const: false, + }], is_none: false } + } +} + +impl Parse for CtorEnumConfiguration { + fn parse(input: ParseStream) -> syn::Result { + let mut configuration = CtorEnumConfiguration::default(); + loop { + match input.parse::()? { + EnumConfigItem::Visibility { visibility } => configuration.default_visibility = visibility, + EnumConfigItem::Prefix { prefix } => configuration.prefix = Some(prefix), + } + if input.parse::().is_err() { + break; + } + } + Ok(configuration) + } +} + +impl Parse for EnumConfigItem { + fn parse(input: ParseStream) -> syn::Result { + let property = input.parse::()?; + let property_name = property.to_string(); + + input.parse::()?; + + Ok(match property_name.as_str() { + "vis" | "visibility" => EnumConfigItem::Visibility { visibility: input.parse()? }, + "prefix" => EnumConfigItem::Prefix { prefix: input.parse()? }, + _ => return Err(Error::new( + property.span(), + CONFIG_PROP_ERR_MSG.replace("{prop}", &property_name).replace("{values}", ENUM_CTOR_PROPS) + )) + }) + } +} + +pub(crate) fn create_enum_token_stream(derive_input: DeriveInput) -> TokenStream { + if let Data::Enum(data) = derive_input.data { + let configuration = match try_parse_attributes_with_default(&derive_input.attrs, || CtorEnumConfiguration::default()) { + Ok(config) => config, + Err(err) => return TokenStream::from(err.to_compile_error()) + }; + + return create_ctor_enum_impl( + derive_input.ident, + derive_input.generics, + data.variants, + configuration + ) + } + panic!("Expected Enum data") +} + +fn create_ctor_enum_impl( + ident: Ident, + generics: Generics, + variants: Punctuated, + configuration: CtorEnumConfiguration +) -> TokenStream { + let mut methods = Vec::new(); + + for variant in variants { + let variant_code = match &variant.fields { + Fields::Named(_) => 0, + Fields::Unnamed(_) => 1, + Fields::Unit => 2 + }; + + let variant_name = variant.ident; + let variant_config = match try_parse_attributes_with_default(&variant.attrs, || CtorStructConfiguration::from_variant( + &configuration, + variant_name.clone() + )) { + Ok(config) => config, + Err(err) => return TokenStream::from(err.to_compile_error()) + }; + + // stop generation of method if none + if variant_config.is_none { + continue; + } + + let meta = match generate_ctor_meta_from_fields(variant.fields, variant_config.definitions.len()) { + Ok(meta) => meta, + Err(err) => return TokenStream::from(err.into_compile_error()), + }; + + let field_idents = meta.field_idents; + + for (i, definition) in variant_config.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 = match convert_to_snakecase(definition.ident) { + Ok(snake_case_ident) => snake_case_ident, + Err(err) => return TokenStream::from(err.to_compile_error()) + }; + + let const_tkn = if definition.is_const { quote! { const } } else { quote!{} }; + + let enum_generation = if variant_code == 0 { + quote! { Self::#variant_name { #(#field_idents),* } } + } else if variant_code == 1 { + quote! { Self::#variant_name ( #(#field_idents),* ) } + } else { + quote! { Self::#variant_name } + }; + + methods.push(quote! { + #visibility #const_tkn fn #name(#(#method_req_fields),*) -> Self { + #(#method_gen_fields)* + #enum_generation + } + }) + } + } + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + TokenStream::from(quote! { + impl #impl_generics #ident #ty_generics #where_clause { + #(#methods)* + } + }) +} + +fn convert_to_snakecase(method_ident: Ident) -> Result { + let mut snake_case = String::new(); + let mut was_lower = false; + + let ident_string = method_ident.to_string(); + + let mut ident_chars = ident_string.chars(); + loop { + let c = match ident_chars.next() { + Some(c) if c == '_' => { + snake_case.push(c.to_ascii_lowercase()); + was_lower = false; + continue; + }, + Some(c) => c, + None => break, + }; + + let lower_or_numeric = c.is_ascii_lowercase() || c.is_numeric(); + + if was_lower && !lower_or_numeric { + snake_case.push('_') + } + + snake_case.push(c.to_ascii_lowercase()); + + was_lower = lower_or_numeric; + } + + syn::parse_str(&snake_case) +} + +#[test] +fn test_convert_to_snakecase() { + assert_eq!(convert_to_snakecase(Ident::new("A", Span::mixed_site())).unwrap(), Ident::new("a", Span::mixed_site())); + assert_eq!(convert_to_snakecase(Ident::new("Test", Span::mixed_site())).unwrap(), Ident::new("test", Span::mixed_site())); + assert_eq!(convert_to_snakecase(Ident::new("Test1", Span::mixed_site())).unwrap(), Ident::new("test1", Span::mixed_site())); + assert_eq!(convert_to_snakecase(Ident::new("ONETWO", Span::mixed_site())).unwrap(), Ident::new("onetwo", Span::mixed_site())); + assert_eq!(convert_to_snakecase(Ident::new("OneTwo", Span::mixed_site())).unwrap(), Ident::new("one_two", Span::mixed_site())); + assert_eq!(convert_to_snakecase(Ident::new("_Abc", Span::mixed_site())).unwrap(), Ident::new("_abc", Span::mixed_site())); + assert_eq!(convert_to_snakecase(Ident::new("A_B", Span::mixed_site())).unwrap(), Ident::new("a_b", Span::mixed_site())); + assert_eq!(convert_to_snakecase(Ident::new("A_b", Span::mixed_site())).unwrap(), Ident::new("a_b", Span::mixed_site())); + assert_eq!(convert_to_snakecase(Ident::new("abCdEf", Span::mixed_site())).unwrap(), Ident::new("ab_cd_ef", Span::mixed_site())); + assert_eq!(convert_to_snakecase(Ident::new("one_2_three", Span::mixed_site())).unwrap(), Ident::new("one_2_three", Span::mixed_site())); + assert_eq!(convert_to_snakecase(Ident::new("ending_", Span::mixed_site())).unwrap(), Ident::new("ending_", Span::mixed_site())); + assert_eq!(convert_to_snakecase(Ident::new("endinG_", Span::mixed_site())).unwrap(), Ident::new("endin_g_", Span::mixed_site())); +} \ No newline at end of file diff --git a/src/fields.rs b/src/fields.rs new file mode 100644 index 0000000..273ebc6 --- /dev/null +++ b/src/fields.rs @@ -0,0 +1,172 @@ +#[cfg(feature = "no-std")] +extern crate alloc; +#[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; + + +use proc_macro2::{Delimiter, Punct}; +use proc_macro2::Spacing::Alone; +use quote::{quote, TokenStreamExt, ToTokens}; +use syn::{Error, Ident, LitInt, token, Type, Token}; +use syn::parse::discouraged::AnyDelimiter; +use syn::parse::{ParseStream, Parse}; +use syn::spanned::Spanned; +use syn::token::{Comma, Impl}; + +use crate::{consume_delimited, CONFIG_PROP_ERR_MSG}; + +static FIELD_PROPS: &str = "\"cloned\", \"default\", \"expr\", \"into\", \"iter\""; + +/// Represents a configuration on a struct field +/// +/// # Example +/// +/// ``` +/// use derive_ctor::ctor; +/// +/// #[derive(ctor)] +/// struct Example { +/// #[ctor(default = [1, 2])] +/// field: i16 +/// } +/// ``` + +#[derive(Clone)] +pub(crate) struct FieldConfig { + pub(crate) property: FieldConfigProperty, + pub(crate) applications: HashSet +} + +#[derive(Clone)] +pub(crate) enum FieldConfigProperty { + Cloned, + Default, + Into, + Iter { iter_type: Type }, + Expression { expression: proc_macro2::TokenStream, input_type: Option, self_referencing: bool } +} + +#[derive(Clone)] +pub(crate) struct ParameterField { + pub(crate) field_ident: Ident, + pub(crate) field_type: Type +} + +#[derive(Clone)] +pub(crate) struct GeneratedField { + pub(crate) field_ident: Ident, + pub(crate) configuration: FieldConfigProperty +} + +impl Parse for FieldConfig { + fn parse(input: ParseStream) -> syn::Result { + let mut config = FieldConfig { + property: input.parse()?, + applications: HashSet::default() + }; + + if input.parse::().is_err() { + return Ok(config) + } + + // consume constructor specifier ex: 1, 2, 3 + if let Ok((delim, span, buffer)) = input.parse_any_delimiter() { + if delim != Delimiter::Bracket { + return Err(Error::new(span.span(), "Expected enclosing brackets")) + } + loop { + config.applications.insert(buffer.parse::()?.base10_parse()?); + if buffer.parse::().is_err() { + break; + } + } + } else { + config.applications.insert(input.parse::()?.base10_parse()?); + } + + Ok(config) + } +} + +impl Parse for FieldConfigProperty { + fn parse(input: ParseStream) -> syn::Result { + if let Ok(token) = input.parse::() { + return Err(Error::new( + token.span(), + "\"impl\" property has been renamed to \"into\"." + )); + } + + let property: Ident = input.parse()?; + let property_name = property.to_string(); + match property_name.as_str() { + "cloned" => Ok(FieldConfigProperty::Cloned), + "default" => Ok(FieldConfigProperty::Default), + "into" => Ok(FieldConfigProperty::Into), + "iter" => consume_delimited(input, Delimiter::Parenthesis, |buffer| { + Ok(FieldConfigProperty::Iter { iter_type: buffer.parse()? }) + }), + "expr" => { + let self_referencing = input.parse::().is_ok(); + + consume_delimited(input, Delimiter::Parenthesis, |buffer| { + let mut input_type = None; + + // determine the input_type by looking for the expression: expr(TYPE -> EXPRESSION) + if buffer.peek2(Token![->]) { + input_type = Some(buffer.parse()?); + buffer.parse::]>()?; + } + + Ok(FieldConfigProperty::Expression { self_referencing, input_type, + expression: proc_macro2::TokenStream::parse(&buffer) + .expect("Unable to convert buffer back into TokenStream") + }) + }) + }, + "method" => Err(Error::new( + property.span(), + "\"method\" property has been removed. Please refer to documentation for a list of valid properties." + )), + _ => Err(Error::new( + property.span(), + CONFIG_PROP_ERR_MSG.replace("{prop}", &property_name).replace("{values}", FIELD_PROPS) + )) + } + } +} + +impl ToTokens for ParameterField { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.field_ident.to_tokens(tokens); + tokens.append(Punct::new(':', Alone)); + self.field_type.to_tokens(tokens); + } +} + +impl ToTokens for GeneratedField { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let ident = &self.field_ident; + + let token_stream = quote! { + let #ident = + }; + + tokens.extend(token_stream); + + tokens.extend(match &self.configuration { + FieldConfigProperty::Cloned => quote! { #ident.clone() }, + FieldConfigProperty::Default => quote! { Default::default() }, + FieldConfigProperty::Expression { expression, .. } => expression.clone(), + FieldConfigProperty::Into => quote! { #ident.into() }, + FieldConfigProperty::Iter { .. } => quote! { #ident.into_iter().collect() }, + }); + + tokens.append(Punct::new(';', Alone)) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 8e68f0f..eae64b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,324 +1,80 @@ -#![cfg_attr(feature = "no-std", no_std, doc = "Test")] +#![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::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::vec; -#[cfg(not(feature = "no-std"))] -use std::vec::Vec; -#[cfg(not(feature = "no-std"))] -use std::collections::HashSet; - - use proc_macro::TokenStream; - -use proc_macro2::{Delimiter, Punct, Span}; -use proc_macro2::Spacing::Alone; -use quote::{quote, TokenStreamExt, ToTokens}; -use syn::{Attribute, Data, DeriveInput, Error, Fields, Generics, Ident, LitInt, parse::Parse, parse_macro_input, token, Type, Visibility}; +use proc_macro2::Delimiter; +use quote::ToTokens; +use structs::create_struct_token_stream; use syn::parse::discouraged::AnyDelimiter; +use syn::parse::Parse; use syn::parse::ParseStream; +use syn::parse_macro_input; use syn::spanned::Spanned; -use syn::token::{Comma, Impl, Pub}; - - - -static FIELD_CONFIG_ERR_MSG: &str = - "Unexpected property: \"{prop}\" (must be one of the following:\ - \"default\", \"expr\", \"impl\")"; - -struct CtorTypeConfiguration { - definitions: Vec<(Visibility, Ident)> -} - -impl Default for CtorTypeConfiguration { - fn default() -> Self { - Self { definitions: vec![(Visibility::Public(Pub { span: Span::call_site() }), Ident::new("new", Span::mixed_site()))] } - } -} - -/// Represents a configuration on a struct field -/// -/// # Example -/// -/// ``` -/// use derive_ctor::ctor; -/// -/// #[derive(ctor)] -/// struct Example { -/// #[ctor(default = [1, 2])] -/// field: i16 -/// } -/// ``` - -#[derive(Clone)] -struct FieldConfig { - property: FieldConfigProperty, - applications: HashSet -} - -#[derive(Clone)] -enum FieldConfigProperty { - Default, - Impl, - Expression { expression: proc_macro2::TokenStream } -} - -struct RequiredStructField { - field_ident: Ident, - field_type: Type -} - -struct GeneratedStructField { - field_ident: Ident, - configuration: FieldConfigProperty -} +use syn::Attribute; +use syn::Data; +use syn::DeriveInput; +use syn::Error; +use syn::Type; +use crate::enums::create_enum_token_stream; -impl Parse for CtorTypeConfiguration { - fn parse(input: ParseStream) -> syn::Result { - if input.is_empty() { - return Ok(Self::default()) - } - - let mut definitions = Vec::new(); - - loop { - if !input.peek(syn::Ident) { - definitions.push((input.parse()?, input.parse()?)); - } else { - definitions.push((Visibility::Inherited, input.parse()?)); - } - - // Consume a comma to continue looking for constructors - if input.parse::().is_err() { - break; - } - } - - Ok(Self { definitions }) - } -} - -impl Parse for FieldConfig { - fn parse(input: ParseStream) -> syn::Result { - let mut config = FieldConfig { - property: input.parse()?, - applications: HashSet::default() - }; - - if input.parse::().is_err() { - return Ok(config) - } - - // consume constructor specifier ex: 1, 2, 3 - if let Ok((delim, span, buffer)) = input.parse_any_delimiter() { - if delim != Delimiter::Bracket { - return Err(Error::new(span.span(), "Expected enclosing brackets")) - } - loop { - config.applications.insert(buffer.parse::()?.base10_parse()?); - if buffer.parse::().is_err() { - break; - } - } - } else { - config.applications.insert(input.parse::()?.base10_parse()?); - } - - Ok(config) - } -} - -impl Parse for FieldConfigProperty { - fn parse(input: ParseStream) -> syn::Result { - if input.parse::().is_ok() { - return Ok(FieldConfigProperty::Impl); - } - - let property: Ident = input.parse()?; - let property_name = property.to_string(); - match property_name.as_str() { - "default" => Ok(FieldConfigProperty::Default), - "expr" => { - let (delimiter, span, buffer) = input.parse_any_delimiter()?; - - if delimiter != Delimiter::Parenthesis { - return Err(Error::new(span.span(), "Expected enclosing parenthesis")) - } - - Ok(FieldConfigProperty::Expression { - expression: proc_macro2::TokenStream::parse(&buffer) - .expect("Unable to convert buffer back into TokenStream") - }) - }, - "method" => Err(Error::new( - property.span(), - "\"method\" property has been removed. Please refer to documentation for a list of valid properties." - )), - _ => Err(Error::new( - property.span(), - FIELD_CONFIG_ERR_MSG.replace("{prop}", &property_name) - )) - } - } -} - -impl ToTokens for RequiredStructField { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - self.field_ident.to_tokens(tokens); - tokens.append(Punct::new(':', Alone)); - self.field_type.to_tokens(tokens); - } -} - -impl ToTokens for GeneratedStructField { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let ident = &self.field_ident; - - let token_stream = quote! { - let #ident = - }; - - tokens.extend(token_stream); - - tokens.extend(match &self.configuration { - FieldConfigProperty::Default => quote! { Default::default() }, - FieldConfigProperty::Expression { expression } => expression.clone(), - FieldConfigProperty::Impl => quote! { #ident.into() } - }); - - tokens.append(Punct::new(';', Alone)) - } -} +pub(crate) mod enums; +pub(crate) mod structs; +pub(crate) mod fields; +pub(crate) static CONFIG_PROP_ERR_MSG: &str = + "Unexpected property: \"{prop}\" (must be one of the following: \"{values}\")"; #[proc_macro_derive(ctor, attributes(ctor))] pub fn derive_ctor(input: TokenStream) -> TokenStream { let derive_input = parse_macro_input!(input as DeriveInput); - let configuration = match try_parse_type_attributes(&derive_input.attrs) { - Ok(config) => config, - Err(err) => return TokenStream::from(err.to_compile_error()) - }; - - match derive_input.data { - Data::Struct(data) => create_ctor_struct_impl( - derive_input.ident, - derive_input.generics, - data.fields, - configuration - ), - Data::Enum(_) => TokenStream::from(Error::new(derive_input.span(), "Enums are not yet supported by ctor").to_compile_error()), + match &derive_input.data { + Data::Struct(_) => create_struct_token_stream(derive_input), + Data::Enum(_) => create_enum_token_stream(derive_input), Data::Union(_) => TokenStream::from(Error::new(derive_input.span(), "Unions are not yet supported by ctor").to_compile_error()) } } -fn create_ctor_struct_impl( - ident: Ident, - generics: Generics, - fields: Fields, - struct_configuration: CtorTypeConfiguration -) -> TokenStream { - let mut required_fields = Vec::new(); - let mut generated_fields = Vec::new(); - - let mut field_idents= Vec::new(); - - let method_count = struct_configuration.definitions.len(); - for _i in 0..method_count { - required_fields.push(Vec::new()); - generated_fields.push(Vec::new()); - } - - for field in fields { - let configuration = match try_parse_field_attributes(&field.attrs) { - Ok(config) => config, - Err(err) => return TokenStream::from(err.to_compile_error()) - }; - - let field_ident = field.ident.expect("Missing struct field name"); - field_idents.push(field_ident.clone()); - - for i in 0..method_count { - - let field_ident = field_ident.clone(); - let field_type = field.ty.clone(); - - match &configuration { - None => required_fields[i].push(RequiredStructField { field_ident, field_type }), - Some(configuration) => { - if matches!(configuration.property, FieldConfigProperty::Impl) { - generated_fields[i].push(GeneratedStructField { - field_ident: field_ident.clone(), - configuration: configuration.property.clone() - }); - required_fields[i].push(RequiredStructField { field_ident, - field_type: syn::parse2(quote! { impl Into<#field_type> }).unwrap() - }); - continue; - } - - let applications = &configuration.applications; - if applications.is_empty() || applications.contains(&i) { - generated_fields[i].push(GeneratedStructField { - field_ident, - configuration: configuration.property.clone() - }) - } else { - required_fields[i].push(RequiredStructField { field_ident, field_type }) - } - } - } +pub(crate) fn try_parse_attributes_with_default T>(attributes: &[Attribute], default: F) -> Result { + for attribute in attributes { + if attribute.path().is_ident("ctor") { + return attribute.parse_args::() } } - - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - - let mut methods = Vec::new(); - for (i, (visibility, name)) in struct_configuration.definitions.into_iter().enumerate() { - let method_req_fields = &required_fields[i]; - let method_gen_fields = &generated_fields[i]; - methods.push(quote! { - #visibility fn #name(#(#method_req_fields),*) -> Self { - #(#method_gen_fields)* - Self { #(#field_idents),* } - } - }) - } - - - TokenStream::from(quote! { - impl #impl_generics #ident #ty_generics #where_clause { - #(#methods)* - } - }) + Ok(default()) } -fn try_parse_type_attributes(attributes: &[Attribute]) -> Result { +pub(crate) fn try_parse_attributes(attributes: &[Attribute]) -> Result, Error> { for attribute in attributes { if attribute.path().is_ident("ctor") { - return attribute.parse_args::(); + return attribute.parse_args::().map(|t| Some(t)) } } - Ok(CtorTypeConfiguration::default()) + Ok(None) } -fn try_parse_field_attributes(attributes: &[Attribute]) -> Result, Error> { - for attribute in attributes { - if attribute.path().is_ident("ctor") { - return attribute.parse_args::() - .map(|config| Some(config)) +pub(crate) fn is_phantom_data(typ: &Type) -> bool { + for token in typ.to_token_stream() { + if token.to_string() == "PhantomData" { + return true; } } - Ok(None) + false +} + +pub(crate) fn consume_delimited(stream: ParseStream, expected: Delimiter, expression: F) -> Result +where F: Fn(ParseStream) -> Result { + let (delimiter, span, buffer) = stream.parse_any_delimiter()?; + if delimiter != expected { + return Err(Error::new(span.span(), format!("Expected enclosing {:?}", expected))) + } + expression(&buffer) +} + + +#[test] +fn test_is_phantom_data() { + assert!(is_phantom_data(&syn::parse_str::("PhantomData").unwrap())); + assert!(is_phantom_data(&syn::parse_str::("&mut PhantomData<&'static str>").unwrap())); + assert!(!is_phantom_data(&syn::parse_str::("i32").unwrap())); } \ No newline at end of file diff --git a/src/structs.rs b/src/structs.rs new file mode 100644 index 0000000..b119474 --- /dev/null +++ b/src/structs.rs @@ -0,0 +1,229 @@ +#[cfg(feature = "no-std")] +extern crate alloc; +#[cfg(feature = "no-std")] +use alloc::vec; +#[cfg(feature = "no-std")] +use alloc::vec::Vec; +#[cfg(feature = "no-std")] + +#[cfg(not(feature = "no-std"))] +use std::vec; +#[cfg(not(feature = "no-std"))] +use std::vec::Vec; + +use proc_macro::TokenStream; + +use proc_macro2::Span; +use quote::quote; +use syn::{parse2, Data, DeriveInput, Error, Fields, Generics, Ident, Visibility}; +use syn::parse::{ParseStream, Parse}; +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}; + +pub(crate) struct CtorDefinition { + pub(crate) visibility: Visibility, + pub(crate) ident: Ident, + pub(crate) is_const: bool +} + +impl Default for CtorDefinition { + fn default() -> Self { + Self { + visibility: Visibility::Public(Pub { span: Span::call_site() }), + ident: Ident::new("new", Span::mixed_site()), + is_const: false + } + } +} + +pub(crate) struct CtorStructConfiguration { + pub(crate) definitions: Vec, + pub(crate) is_none: bool +} + +impl Default for CtorStructConfiguration { + fn default() -> Self { + Self { definitions: vec![CtorDefinition::default()], is_none: false } + } +} + +impl Parse for CtorStructConfiguration { + fn parse(input: ParseStream) -> syn::Result { + if input.is_empty() { + return Ok(Self::default()) + } + + let mut definitions = Vec::new(); + + loop { + let mut is_const = input.parse::().is_ok(); + + 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 + CtorDefinition { + visibility, + ident: input.parse()?, + is_const + } + } 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 }) + } + + CtorDefinition { + visibility: Visibility::Inherited, + ident, + is_const + } + }; + + definitions.push(definition); + + // Consume a comma to continue looking for constructors + if input.parse::().is_err() { + break; + } + } + + Ok(Self { definitions, is_none: false }) + } +} + +pub(crate) fn create_struct_token_stream(derive_input: DeriveInput) -> TokenStream { + if let Data::Struct(data) = derive_input.data { + let configuration = match try_parse_attributes_with_default(&derive_input.attrs, || CtorStructConfiguration::default()) { + Ok(config) => config, + Err(err) => return TokenStream::from(err.to_compile_error()) + }; + + return create_ctor_struct_impl( + derive_input.ident, + derive_input.generics, + data.fields, + configuration + ) + } + panic!("Expected Struct data") +} + +fn create_ctor_struct_impl( + ident: Ident, + generics: Generics, + fields: Fields, + configuration: CtorStructConfiguration +) -> TokenStream { + let meta = match generate_ctor_meta_from_fields(fields, configuration.definitions.len()) { + Ok(meta) => meta, + Err(err) => return TokenStream::from(err.into_compile_error()), + }; + + let field_idents = meta.field_idents; + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let mut methods = Vec::new(); + 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!{} }; + + methods.push(quote! { + #visibility #const_tkn fn #name(#(#method_req_fields),*) -> Self { + #(#method_gen_fields)* + Self { #(#field_idents),* } + } + }) + } + + + TokenStream::from(quote! { + impl #impl_generics #ident #ty_generics #where_clause { + #(#methods)* + } + }) +} + +pub(crate) struct ConstructorMeta { + pub(crate) parameter_fields: Vec>, + pub(crate) generated_fields: Vec>, + pub(crate) field_idents: Vec +} + +impl ConstructorMeta { + pub(crate) fn new(method_count: usize) -> ConstructorMeta { + ConstructorMeta { + parameter_fields: vec![Default::default(); method_count], + generated_fields: vec![Default::default(); method_count], + field_idents: Default::default() + } + } +} + + +pub(crate) fn generate_ctor_meta_from_fields(fields: Fields, method_count: usize) -> Result { + let mut meta = ConstructorMeta::new(method_count); + for (field_index, field) in fields.into_iter().enumerate() { + let configuration = try_parse_attributes::(&field.attrs)?; + + let field_ident = field.ident.unwrap_or_else(|| { + Ident::new(&("arg".to_owned() + &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(); + let ft = &field.ty; + + let mut req_field_type = None; + let mut gen_configuration = None; + + match &configuration { + None if is_phantom_data(&field.ty) => gen_configuration = Some(FieldConfigProperty::Default), + None => req_field_type = Some(field.ty.clone()), + Some(configuration) => { + let applications = &configuration.applications; + gen_configuration = Some(configuration.property.clone()); + + if applications.is_empty() || applications.contains(&i) { + // create a required field type if the configuration requires an additional input parameter + req_field_type = match &configuration.property { + FieldConfigProperty::Cloned => Some(parse2(quote! { &#ft }).unwrap()), + FieldConfigProperty::Into => Some(parse2(quote! { impl Into<#ft> }).unwrap()), + FieldConfigProperty::Iter { iter_type } => Some(parse2(quote! { impl IntoIterator }).unwrap()), + FieldConfigProperty::Expression { input_type, .. } if input_type.is_some() => input_type.clone(), + FieldConfigProperty::Expression { self_referencing, .. } if *self_referencing => Some(field.ty.clone()), + _ => None + } + } else if is_phantom_data(&field.ty) { + gen_configuration = Some(FieldConfigProperty::Default); + } else { + gen_configuration = None; + req_field_type = Some(field.ty.clone()); + } + } + } + + if let Some(cfg) = gen_configuration { + meta.generated_fields[i].push(GeneratedField { + field_ident: field_ident.clone(), + configuration: cfg + }) + } + if let Some(field_type) = req_field_type { + meta.parameter_fields[i].push(ParameterField { field_ident, field_type }) + } + } + } + Ok(meta) +} \ No newline at end of file diff --git a/tests/enum_base.rs b/tests/enum_base.rs new file mode 100644 index 0000000..99d7ac1 --- /dev/null +++ b/tests/enum_base.rs @@ -0,0 +1,47 @@ +use derive_ctor::ctor; + +#[derive(ctor, Debug, PartialEq)] +enum EnumUnitVariant { + Variant1 +} + +#[derive(ctor, Debug, PartialEq)] +enum EnumStructVariant { + Variant { field: i32 } +} + +#[derive(ctor, Debug, PartialEq)] +enum EnumNamelessVariant { + Variant(i32) +} + +#[test] +fn test_enum_variants() { + let unit = EnumUnitVariant::variant1(); + assert_eq!(EnumUnitVariant::Variant1, unit); + + let struct_variant = EnumStructVariant::variant(13); + assert_eq!(EnumStructVariant::Variant { field: 13 }, struct_variant); + + let nameless_variant = EnumNamelessVariant::variant(95); + assert_eq!(EnumNamelessVariant::Variant(95), nameless_variant); +} + +#[derive(ctor, Debug, PartialEq)] +enum MultipleVariants { + Variant1, + Variant2(i32), + Variant3 { value: usize } +} + +#[test] +fn test_enum_multiple_variants() { + let v1 = MultipleVariants::variant1(); + assert_eq!(MultipleVariants::Variant1, v1); + + let v2 = MultipleVariants::variant2(123); + assert_eq!(MultipleVariants::Variant2(123), v2); + + let v3 = MultipleVariants::variant3(888); + assert_eq!(MultipleVariants::Variant3 { value: 888 }, v3); +} \ No newline at end of file diff --git a/tests/enum_default_config.rs b/tests/enum_default_config.rs new file mode 100644 index 0000000..9ed48be --- /dev/null +++ b/tests/enum_default_config.rs @@ -0,0 +1,43 @@ +use derive_ctor::ctor; + +#[derive(ctor, Debug, PartialEq)] +#[ctor(prefix = new)] +enum PrefixEnum { + Variant1, + Variant2(i32), + Variant3 { value: i32 } +} + +#[test] +fn test_variants_with_prefix() { + let p1 = PrefixEnum::new_variant1(); + let p2 = PrefixEnum::new_variant2(123); + let p3 = PrefixEnum::new_variant3(456); + assert_eq!(PrefixEnum::Variant1, p1); + assert_eq!(PrefixEnum::Variant2(123), p2); + assert_eq!(PrefixEnum::Variant3 { value: 456 }, p3) +} + +#[derive(ctor)] +#[ctor(vis = pub(crate))] +enum VisibilityEnum { + Element +} + +#[test] +fn test_visibility_enum() { + let result = VisibilityEnum::element(); + assert!(matches!(result, VisibilityEnum::Element)); +} + +#[derive(ctor)] +#[ctor(prefix = new, visibility = pub(crate))] +enum PrefixAndVisibilityEnum { + Element +} + +#[test] +fn test_visibility_and_prefix_enum() { + let result = PrefixAndVisibilityEnum::new_element(); + assert!(matches!(result, PrefixAndVisibilityEnum::Element)) +} \ No newline at end of file diff --git a/tests/enum_unnamed_variants.rs b/tests/enum_unnamed_variants.rs new file mode 100644 index 0000000..6c21db9 --- /dev/null +++ b/tests/enum_unnamed_variants.rs @@ -0,0 +1,43 @@ +use derive_ctor::ctor; + +#[derive(ctor, Debug, PartialEq)] +enum UnnamedVariantEnum { + One(#[ctor(default)] i32) +} + +#[test] +fn test_unnamed_variant_property() { + let result = UnnamedVariantEnum::one(); + assert_eq!(UnnamedVariantEnum::One(0), result) +} + +#[derive(ctor, Debug, PartialEq)] +enum UnnamedVariantEnumMultipleFields { + Many(i32, #[ctor(expr(false))] bool, #[ctor(into)] String) +} + +#[test] +fn test_unnamed_variant_multiple_properties() { + let result = UnnamedVariantEnumMultipleFields::many(50, "FooBar"); + assert_eq!(UnnamedVariantEnumMultipleFields::Many(50, false, String::from("FooBar")), result); +} + +#[derive(ctor, Debug, PartialEq)] +enum EnumMultipleUnnamedVariants { + One(i32), + Two(bool, #[ctor(default)] String), + Three(#[ctor(cloned)] String) +} + +#[test] +fn test_multiple_enum_variants() { + let one = EnumMultipleUnnamedVariants::one(13); + assert_eq!(EnumMultipleUnnamedVariants::One(13), one); + + let two = EnumMultipleUnnamedVariants::two(false); + assert_eq!(EnumMultipleUnnamedVariants::Two(false, String::default()), two); + + let s = String::from("Test"); + let three = EnumMultipleUnnamedVariants::three(&s); + assert_eq!(EnumMultipleUnnamedVariants::Three(s), three); +} \ No newline at end of file diff --git a/tests/enum_variant_config.rs b/tests/enum_variant_config.rs new file mode 100644 index 0000000..bf73d57 --- /dev/null +++ b/tests/enum_variant_config.rs @@ -0,0 +1,51 @@ +use derive_ctor::ctor; + +#[derive(ctor, Debug, PartialEq)] +enum SpecificVariantMethod { + #[ctor(const pub new, other)] + Variant1(i32) +} + +#[test] +fn test_variant_with_configured_ctors() { + const RESULT: SpecificVariantMethod = SpecificVariantMethod::new(15); + assert_eq!(SpecificVariantMethod::Variant1(15), RESULT); + + let result2 = SpecificVariantMethod::other(30); + assert_eq!(SpecificVariantMethod::Variant1(30), result2); +} + +#[derive(ctor, Debug, PartialEq)] +enum EnumNoVariantGeneration { + Variant1, + #[ctor(none)] + Variant2 +} + +#[test] +fn test_enum_with_specified_no_ctor_generation() { + let variant = EnumNoVariantGeneration::variant1(); + assert_eq!(EnumNoVariantGeneration::Variant1, variant); // you'll have to take my word for it- variant2 does not get generated +} + +#[derive(ctor, Debug, PartialEq)] +#[ctor(prefix = new, vis = pub)] +enum VariantConfigOverridesDefaults { + Variant1, + #[ctor(variant2)] + Variant2, + #[ctor(none)] + Variant3 +} + +#[test] +fn test_variant_config_overrides_default_values() { + let variant1 = VariantConfigOverridesDefaults::new_variant1(); + assert_eq!(VariantConfigOverridesDefaults::Variant1, variant1); + + let variant2 = VariantConfigOverridesDefaults::variant2(); + assert_eq!(VariantConfigOverridesDefaults::Variant2, variant2); + + // variant3 was not generated +} + diff --git a/tests/struct_base.rs b/tests/struct_base.rs index 1e04ad9..7906bef 100644 --- a/tests/struct_base.rs +++ b/tests/struct_base.rs @@ -39,4 +39,45 @@ pub struct ManyFieldStruct { fn test_struct_with_many_fields() { let mfs = ManyFieldStruct::new(400, true); assert_eq!(ManyFieldStruct { value1: 400, value2: true }, mfs); +} + +#[derive(ctor, Debug, PartialEq)] +pub struct GenericStruct { + item: T +} + +#[derive(ctor, Debug, PartialEq)] +pub struct WhereStruct +where T: Into { + item: T +} + +#[derive(ctor, Debug, PartialEq)] +pub struct StructWithClosure { + closure: fn(usize) -> bool +} + +#[derive(ctor, Debug, PartialEq)] +pub struct StructWithClosureGeneric +where F: Fn(usize) -> bool { + closure: F +} + +#[test] +fn test_generic_structs() { + let generic1: GenericStruct = GenericStruct::new(400); + assert_eq!(GenericStruct { item: 400 as usize }, generic1); + + let generic2: WhereStruct<&'static str> = WhereStruct::new("FooBar"); + assert_eq!(WhereStruct { item: "FooBar" }, generic2); +} + +#[test] +fn test_closure_structs() { + let closure1 = StructWithClosure::new(|us| us == 439); + assert!((closure1.closure)(439)); + + let target = 440; + let closure2 = StructWithClosureGeneric::new(|us| us == target); + assert!((closure2.closure)(440)) } \ No newline at end of file diff --git a/tests/struct_const_ctor_config.rs b/tests/struct_const_ctor_config.rs new file mode 100644 index 0000000..abfff45 --- /dev/null +++ b/tests/struct_const_ctor_config.rs @@ -0,0 +1,52 @@ +use derive_ctor::ctor; + +#[derive(ctor, Debug, PartialEq)] +#[ctor(const new)] +struct ConstStructV0 { + value: i32 +} + +#[derive(ctor, Debug, PartialEq)] +#[ctor(const pub new)] +struct ConstStructV1 { + value: i32 +} + +#[derive(ctor, Debug, PartialEq)] +#[ctor(pub const new)] +struct ConstStructV2 { + value: i32 +} + +#[derive(ctor, Debug, PartialEq)] +#[ctor(m1, pub(crate) const m2, const m3, m4)] +struct ConstStructMultiple { + value: i32 +} + +#[test] +fn test_const_struct_variations() { + const CONST_STRUCT_V0: ConstStructV0 = ConstStructV0::new(1); + assert_eq!(ConstStructV0 { value: 1 }, CONST_STRUCT_V0); + + const CONST_STRUCT_V1: ConstStructV1 = ConstStructV1::new(2); + assert_eq!(ConstStructV1 { value: 2 }, CONST_STRUCT_V1); + + const CONST_STRUCT_V2: ConstStructV2 = ConstStructV2::new(3); + assert_eq!(ConstStructV2 { value: 3 }, CONST_STRUCT_V2); +} + +#[test] +fn test_struct_with_multiple_methods() { + let v0: ConstStructMultiple = ConstStructMultiple::m1(1); + assert_eq!(ConstStructMultiple { value: 1 }, v0); + + const V1: ConstStructMultiple = ConstStructMultiple::m2(2); + assert_eq!(ConstStructMultiple { value: 2 }, V1); + + const V2: ConstStructMultiple = ConstStructMultiple::m3(3); + assert_eq!(ConstStructMultiple { value: 3 }, V2); + + let v3: ConstStructMultiple = ConstStructMultiple::m4(4); + assert_eq!(ConstStructMultiple { value: 4 }, v3); +} \ No newline at end of file diff --git a/tests/struct_method_config.rs b/tests/struct_ctor_config.rs similarity index 100% rename from tests/struct_method_config.rs rename to tests/struct_ctor_config.rs diff --git a/tests/struct_field_all.rs b/tests/struct_field_all.rs index 0fa82e5..0737762 100644 --- a/tests/struct_field_all.rs +++ b/tests/struct_field_all.rs @@ -4,8 +4,14 @@ use derive_ctor::ctor; struct MixedStruct { provided1: i16, provided2: bool, - #[ctor(impl)] + #[ctor(into)] provided3: String, + #[ctor(cloned)] + provided4: String, + #[ctor(expr!(partial1 + 100))] + partial1: u32, + #[ctor(expr(i32 -> partial2 < 0))] + partial2: bool, #[ctor(expr("Foo"))] generated1: &'static str, #[ctor(default)] @@ -14,6 +20,17 @@ struct MixedStruct { #[test] fn test_struct_with_multiple_generated_fields() { - let multi = MixedStruct::new(41, false, "Test"); - assert_eq!(MixedStruct { provided1: 41, provided2: false, provided3: String::from("Test"), generated1: "Foo", generated2: Default::default() }, multi) + let provided4 = String::from("Bar"); + + let multi = MixedStruct::new(41, false, "Test", &provided4, 90, -1238); + assert_eq!(MixedStruct { + provided1: 41, + provided2: false, + provided3: String::from("Test"), + provided4, + partial1: 190, + partial2: true, + generated1: "Foo", + generated2: Default::default() + }, multi) } \ No newline at end of file diff --git a/tests/struct_field_cloned.rs b/tests/struct_field_cloned.rs new file mode 100644 index 0000000..88aadcb --- /dev/null +++ b/tests/struct_field_cloned.rs @@ -0,0 +1,14 @@ +use derive_ctor::ctor; + +#[derive(ctor, Debug, PartialEq)] +struct StructClone { + #[ctor(cloned)] + value: String +} + +#[test] +fn test_struct_clone_field() { + let value = String::from("Foo"); + let test = StructClone::new(&value); + assert_eq!(StructClone { value, }, test) +} \ No newline at end of file diff --git a/tests/struct_field_expr.rs b/tests/struct_field_expr.rs index 51682ac..0ce5674 100644 --- a/tests/struct_field_expr.rs +++ b/tests/struct_field_expr.rs @@ -52,4 +52,41 @@ fn test_struct_complex_expr_field() { fn test_struct_reliant_expr_field() { let test = StructReliantExpr::new(103); assert_eq!(StructReliantExpr { provided: 103, generated: 103.to_string() }, test); +} + +#[derive(ctor, Debug, PartialEq)] +struct SelfRefExpr { + #[ctor(expr!(value - 1))] + value: u32 +} + +#[derive(ctor, Debug, PartialEq)] +struct ComplexSelfRefExpr { + #[ctor(expr!(n1 - n2))] + n1: u32, + n2: u32 +} + +#[derive(ctor, Debug, PartialEq)] +struct ChangeInputTypeExpr { + #[ctor(expr(String -> Box::new(value)))] + value: Box +} + +#[test] +fn test_self_referencing_expr_field() { + let test = SelfRefExpr::new(105); + assert_eq!(SelfRefExpr { value: 104 }, test); +} + +#[test] +fn test_complex_self_referencing_expr_field() { + let test = ComplexSelfRefExpr::new(489, 69); + assert_eq!(ComplexSelfRefExpr { n1: 420, n2: 69 }, test); +} + +#[test] +fn test_changed_input_type() { + let test = ChangeInputTypeExpr::new(String::from("ABC")); + assert_eq!(ChangeInputTypeExpr { value: Box::new(String::from("ABC"))}, test) } \ No newline at end of file diff --git a/tests/struct_field_impl.rs b/tests/struct_field_into.rs similarity index 91% rename from tests/struct_field_impl.rs rename to tests/struct_field_into.rs index b298dc0..13a5722 100644 --- a/tests/struct_field_impl.rs +++ b/tests/struct_field_into.rs @@ -2,7 +2,7 @@ use derive_ctor::ctor; #[derive(ctor, Debug, PartialEq)] struct StructImpl { - #[ctor(impl)] + #[ctor(into)] provided: String, other: bool } @@ -10,9 +10,9 @@ struct StructImpl { #[derive(ctor, Debug, PartialEq)] struct StructManyImpl { provided: bool, - #[ctor(impl)] + #[ctor(into)] one: String, - #[ctor(impl)] + #[ctor(into)] two: String } diff --git a/tests/struct_field_iter.rs b/tests/struct_field_iter.rs new file mode 100644 index 0000000..886d82a --- /dev/null +++ b/tests/struct_field_iter.rs @@ -0,0 +1,21 @@ +use std::collections::HashSet; + +use derive_ctor::ctor; + +#[derive(ctor, Debug, PartialEq)] +struct StructIter { + #[ctor(iter(usize))] + collection: HashSet +} + +#[test] +fn test_struct_with_field_iter() { + let test = StructIter::new(vec![1, 3, 6, 6]); + + let mut expected_set = HashSet::new(); + expected_set.insert(1); + expected_set.insert(3); + expected_set.insert(6); + + assert_eq!(StructIter { collection: expected_set }, test); +} \ No newline at end of file diff --git a/tests/struct_field_specific_ctor.rs b/tests/struct_field_specific_ctor.rs index e7b50bd..75b1093 100644 --- a/tests/struct_field_specific_ctor.rs +++ b/tests/struct_field_specific_ctor.rs @@ -41,7 +41,7 @@ fn test_struct_with_multiple_targeted_generations() { #[derive(ctor, Debug, PartialEq)] #[ctor(m1, m2, m3)] struct TestStructWithFieldWithMultipleTargets { - #[ctor(impl)] + #[ctor(into)] arg1: String, #[ctor(expr(5) = [0, 1])] arg2: u32 diff --git a/tests/struct_phantom_data.rs b/tests/struct_phantom_data.rs new file mode 100644 index 0000000..b2f0ae0 --- /dev/null +++ b/tests/struct_phantom_data.rs @@ -0,0 +1,14 @@ +use std::marker::PhantomData; +use derive_ctor::ctor; + +#[derive(ctor, Debug, PartialEq)] +struct HasPhantomData { + value: u32, + _marker: PhantomData +} + +#[test] +fn test_phantom_data_auto_excluded_as_parameter() { + let pd = HasPhantomData::new(4); + assert_eq!(HasPhantomData { value: 4, _marker: PhantomData }, pd) +} \ No newline at end of file