Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Default support for struct ctor #6

Merged
merged 1 commit into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
44 changes: 34 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Type>`.
- `#[ctor(iter(FROM_TYPE))]` - Change the parameter type for the generated method to `impl IntoIterator<Item=FROM_TYPE>`.
- 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<Type>`.
- **iter(FROM_TYPE)** - Change the parameter type for the generated method to `impl IntoIterator<Item=FROM_TYPE>`.
- Support no-std via `features = ["no-std"]`

## Basic Usage
Expand All @@ -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:
Expand All @@ -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.

Expand All @@ -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
Expand Down
17 changes: 13 additions & 4 deletions src/enums.rs
Original file line number Diff line number Diff line change
@@ -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\"";

Expand Down Expand Up @@ -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 }
}
}
Expand Down Expand Up @@ -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),* } }
Expand Down
9 changes: 6 additions & 3 deletions src/fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
91 changes: 75 additions & 16 deletions src/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
Expand All @@ -15,25 +20,34 @@ 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};

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<CtorAttribute>
}

#[derive(Hash, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum CtorAttribute {
Const,
Default
}

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
attributes: Default::default()
}
}
}
Expand All @@ -58,28 +72,38 @@ impl Parse for CtorStructConfiguration {
let mut definitions = Vec::new();

loop {
let mut is_const = input.parse::<Const>().is_ok();
let mut attributes = HashSet::new();
if input.parse::<Const>().is_ok() {
attributes.insert(CtorAttribute::Const);
}

let definition = if !input.peek(syn::Ident) {
let visibility = input.parse()?;
is_const = input.parse::<Const>().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::<Const>().is_ok() {
attributes.insert(CtorAttribute::Const);
}
CtorDefinition {
visibility,
ident: input.parse()?,
is_const
attributes
}
} else {
let ident = input.parse::<Ident>()?;

// 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
}
};

Expand Down Expand Up @@ -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
})
}

Expand All @@ -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::<FieldConfig>(&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();
Expand Down Expand Up @@ -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 })
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions tests/enum_variant_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -34,7 +34,7 @@ enum VariantConfigOverridesDefaults {
Variant1,
#[ctor(variant2)]
Variant2,
#[ctor(none)]
#[ctor(none)] #[allow(dead_code)]
Variant3
}

Expand Down
Loading