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

Add shorthand feature #15

Merged
merged 1 commit into from
Jun 11, 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 .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ jobs:
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
run: cargo test --verbose --all-features
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.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "derive-ctor"
version = "1.0.5"
version = "1.0.6"
description = "Adds `#[derive(ctor)]` which allows for the auto-generation of struct, enum, and union constructors."
keywords = ["derive", "macro", "trait", "procedural", "no_std"]
authors = ["Evan Cowin"]
Expand All @@ -13,6 +13,7 @@ categories = ["no-std", "rust-patterns"]
[features]
default = ["structs", "enums", "unions"]
enums = ["dep:heck"]
shorthand = []
structs = []
unions = []

Expand Down
43 changes: 30 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ provides various options to customize the generated constructor methods.
- **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>`.
- No reliance on the standard library (no-std out of the box).
- Usability with structs, enums, and unions are toggleable as features (all are enabled by default)

## Basic Usage

Add `derive-ctor` to your `Cargo.toml`:

```toml
[dependencies]
derive-ctor = "1.0.5"
derive-ctor = "1.0.6"
```

Annotate your struct with `#[derive(ctor)]` to automatically generate a `new` constructor:
Expand Down Expand Up @@ -277,9 +278,9 @@ use derive_ctor::ctor;

#[derive(ctor)]
struct MyStruct {
field1: i32,
#[ctor(iter(usize))]
field2: HashSet<usize>
field1: i32,
#[ctor(iter(usize))]
field2: HashSet<usize>
}

let my_struct = MyStruct::new(0, vec![1, 1, 2, 3, 4]);
Expand All @@ -296,17 +297,33 @@ use derive_ctor::ctor;
#[derive(ctor)]
#[ctor(new, with_defaults)]
struct MyStruct {
field1: i32,
#[ctor(default = [1])]
field2: String,
#[ctor(default = 1)] // brackets can be removed if specifying only 1 index
field3: bool,
#[ctor(default = [0, 1])]
field4: u64,
#[ctor(default)] // this is the same as specifying all indices
field5: u64
field1: i32,
#[ctor(default = [1])]
field2: String,
#[ctor(default = 1)] // brackets can be removed if specifying only 1 index
field3: bool,
#[ctor(default = [0, 1])]
field4: u64,
#[ctor(default)] // this is the same as specifying all indices
field5: u64
}

let my_struct1 = MyStruct::new(100, "Foo".to_string(), true);
let my_struct2 = MyStruct::with_defaults(100);
```

### Non-Default Features

**shorthand** - Allows the usage of "shorthand" attributes on fields. For example, instead of `#[ctor(expr(EXPRESSION)]`
you can use `#[expr(EXPRESSION)]` instead.
```rust
use derive_ctor::ctor;

#[derive(ctor)]
struct MyStruct {
#[expr(100)]
value: u32
}

let my_struct = MyStruct::new();
```
87 changes: 44 additions & 43 deletions src/fields.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
extern crate alloc;

use alloc::collections::BTreeSet as HashSet;
use alloc::string::ToString;
use alloc::vec::Vec;

use proc_macro2::{Delimiter, Punct, Span, TokenTree};
use proc_macro2::Spacing::Alone;
use proc_macro2::{Delimiter, Punct, Span};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::parse::discouraged::AnyDelimiter;
use quote::{quote, TokenStreamExt, ToTokens};
use syn::{Attribute, Error, Fields, Ident, LitInt, parse2, token, Token, Type};
use syn::parse::{Parse, ParseStream};
use syn::parse::discouraged::AnyDelimiter;
use syn::spanned::Spanned;
use syn::token::{Comma, Impl};
use syn::{parse2, token, Error, Fields, Ident, LitInt, Token, Type};
use syn::token::Comma;

use crate::constants::{
CONFIG_PROP_ERR_MSG, FIELD_PROP_CLONED as CLONED, FIELD_PROP_DEFAULT as DEFAULT,
FIELD_PROP_EXPR as EXPR, FIELD_PROP_INTO as INTO, FIELD_PROP_ITER as ITER,
};
use crate::{consume_delimited, CtorAttribute, is_phantom_data, try_parse_attributes};
use crate::{consume_delimited, CtorAttribute, is_phantom_data};
use crate::constants::{CONFIG_PROP_ERR_MSG, CTOR_WORD, FIELD_PROP_CLONED as CLONED, FIELD_PROP_DEFAULT as DEFAULT, FIELD_PROP_EXPR as EXPR, FIELD_PROP_INTO as INTO, FIELD_PROP_ITER as ITER};

const FIELD_PROPS: &str = "\"cloned\", \"default\", \"expr\", \"into\", \"iter\"";

Expand Down Expand Up @@ -112,26 +110,7 @@ impl Parse for FieldConfig {
}

impl FieldConfigProperty {
pub fn parse_expr(input: ParseStream) -> syn::Result<Self> {
let self_referencing = input.parse::<Token![!]>().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::<Token![->]>()?;
}

Ok(FieldConfigProperty::Expression { self_referencing, input_type,
expression: proc_macro2::TokenStream::parse(&buffer)
.expect("Unable to convert buffer back into TokenStream")
})
})
}

pub fn is_generated(&self) -> bool {
fn is_generated(&self) -> bool {
match self {
FieldConfigProperty::Cloned => false,
FieldConfigProperty::Default => true,
Expand All @@ -144,13 +123,6 @@ impl FieldConfigProperty {

impl Parse for FieldConfigProperty {
fn parse(input: ParseStream) -> syn::Result<Self> {
if let Ok(token) = input.parse::<Impl>() {
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() {
Expand All @@ -160,11 +132,24 @@ impl Parse for FieldConfigProperty {
ITER => consume_delimited(input, Delimiter::Parenthesis, |buffer| {
Ok(FieldConfigProperty::Iter { iter_type: buffer.parse()? })
}),
EXPR => Self::parse_expr(input),
"method" => Err(Error::new(
property.span(),
"\"method\" property has been removed. Please refer to documentation for a list of valid properties."
)),
EXPR => {
let self_referencing = input.parse::<Token![!]>().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::<Token![->]>()?;
}

Ok(FieldConfigProperty::Expression { self_referencing, input_type,
expression: proc_macro2::TokenStream::parse(buffer)
.expect("Unable to convert buffer back into TokenStream")
})
})
}
_ => Err(Error::new(
property.span(),
CONFIG_PROP_ERR_MSG.replace("{prop}", &property_name).replace("{values}", FIELD_PROPS)
Expand Down Expand Up @@ -203,6 +188,22 @@ impl ToTokens for GeneratedField {
}
}

fn try_parse_field_attributes(attributes: &[Attribute]) -> Result<Option<FieldConfig>, Error> {
for attribute in attributes {
let attr_path = attribute.path();
if attr_path.is_ident(CTOR_WORD) {
return attribute.parse_args().map(Some);
}
let attribute_token_stream = attribute.to_token_stream();
if let Some(TokenTree::Group(group)) = attribute_token_stream.into_iter().skip(1).next() {
if let Ok(property) = parse2::<FieldConfigProperty>(group.stream()) {
return Ok(Some(FieldConfig { property, applications: Default::default() }))
}
}
}
Ok(None)
}

pub(crate) fn generate_ctor_meta(
ctor_attributes: &HashSet<CtorAttribute>,
fields: &Fields,
Expand All @@ -211,7 +212,7 @@ pub(crate) fn generate_ctor_meta(
let mut meta = ConstructorMeta::default();

for (field_index, field) in fields.iter().enumerate() {
let configuration = try_parse_attributes::<FieldConfig>(&field.attrs)?;
let configuration = try_parse_field_attributes(&field.attrs)?;

let span = field.span();

Expand Down
17 changes: 16 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,20 @@ pub(crate) fn create_union_token_stream(_derive_input: DeriveInput) -> TokenStre
"\"unions\" feature must be enabled to use #[derive(ctor)] on unions.").to_compile_error())
}

#[cfg(feature = "shorthand")]
#[proc_macro_derive(ctor, attributes(ctor, cloned, default, expr, into, iter))]
pub fn derive_ctor(input: TokenStream) -> TokenStream {
derive_ctor_internal(input)
}

#[cfg(not(feature = "shorthand"))]
#[proc_macro_derive(ctor, attributes(ctor))]
pub fn derive_ctor(input: TokenStream) -> TokenStream {
derive_ctor_internal(input)
}


fn derive_ctor_internal(input: TokenStream) -> TokenStream {
let derive_input = parse_macro_input!(input as DeriveInput);

match &derive_input.data {
Expand All @@ -97,6 +109,9 @@ pub fn derive_ctor(input: TokenStream) -> TokenStream {
}
}




pub(crate) fn try_parse_attributes_with_default<T: Parse, F: Fn() -> T>(
attributes: &[Attribute],
default: F,
Expand All @@ -112,7 +127,7 @@ pub(crate) fn try_parse_attributes_with_default<T: Parse, F: Fn() -> T>(
pub(crate) fn try_parse_attributes<T: Parse>(attributes: &[Attribute]) -> Result<Option<T>, Error> {
for attribute in attributes {
if attribute.path().is_ident(CTOR_WORD) {
return attribute.parse_args::<T>().map(|t| Some(t));
return attribute.parse_args::<T>().map(Some);
}
}
Ok(None)
Expand Down
32 changes: 32 additions & 0 deletions tests/shorthand_field_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#[cfg(feature = "shorthand")]
use derive_ctor::ctor;

#[cfg(feature = "shorthand")]
#[derive(ctor, Debug, PartialEq)]
struct ShorthandStruct {
#[expr(100)]
value1: u32,
#[cloned]
value2: String,
#[into]
value3: String,
#[iter(usize)]
value4: Vec<usize>,
#[default]
value5: Option<String>
}

#[test]
#[cfg(feature = "shorthand")]
fn test_struct_with_shorthand() {
let string = "Foo".to_string();
let array = [1];
let test = ShorthandStruct::new(&string, "Bar", array);
assert_eq!(ShorthandStruct {
value1: 100,
value2: string,
value3: "Bar".to_string(),
value4: vec![1],
value5: None
}, test);
}