Skip to content

Commit

Permalink
Derive AttributeIdent by default
Browse files Browse the repository at this point in the history
  • Loading branch information
ModProg committed Aug 6, 2024
1 parent 34ba680 commit fc35f8f
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 100 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]
### Added
- derive `AttributeIdent` by default, converting the `StructIdent` to `snake_case`
- `AttributeIdent` implementation for `Option<impl AttributeIdent>`

## [0.9.2] - 2024-06-22
Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ edition = "2021"

[dependencies]
derive-where = "1.2.7"
manyhow = "0.11.2"
manyhow = "0.11.3"
proc-macro2 = "1"
quote = "1"
syn = "2"
Expand All @@ -34,6 +34,7 @@ path = "macro"
[dev-dependencies]
insta = "1.39.0"
quote = "1"
static_assertions = "1.1.0"
syn = { version = "2", features = ["extra-traits"] }

[package.metadata.release]
Expand Down
229 changes: 141 additions & 88 deletions macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt::Display;
use std::iter;
use std::ops::Range;

use collection_literals::hash;
use interpolator::{format, Formattable};
Expand All @@ -10,8 +11,8 @@ use proc_macro2::{Literal, Span, TokenStream};
use proc_macro_utils::{TokenParser, TokenStream2Ext};
use quote::{format_ident, ToTokens};
use quote_use::quote_use as quote;
use syn::{spanned::Spanned, Visibility};
use syn::{DataStruct, DeriveInput, Field, Fields, Generics, Ident, LitStr, Type};
use syn::spanned::Spanned;
use syn::{DataStruct, DeriveInput, Field, Fields, Generics, Ident, LitStr, Type, Visibility};

const ATTRIBUTE_IDENT: &str = "attribute";

Expand Down Expand Up @@ -170,8 +171,13 @@ struct StructAttrs {
}

impl StructAttrs {
fn from_attrs(attrs: impl IntoIterator<Item = syn::Attribute>) -> Result<Self> {
const VALID_FORMAT: &str = r#"expected `#[attribute(ident=attribute_name, aliases=[alias1, alias2], error="..", error(unknown_field="..", unknown_field_single="..", unknown_field_empty="..", duplicate_field="..", missing_field="..", field_help=".."))]`"#;
fn from_attrs(
struct_ident: &Ident,
attrs: impl IntoIterator<Item = syn::Attribute>,
) -> Result<Self> {
const VALID_FORMAT: &str = r#"expected `#[attribute(ident=attribute_name/!ident, aliases=[alias1, alias2], error="..", error(unknown_field="..", unknown_field_single="..", unknown_field_empty="..", duplicate_field="..", missing_field="..", field_help=".."))]`"#;

let mut ident_span: Option<Range<Span>> = None;
let mut ident: Option<Ident> = None;
let mut aliases: Vec<String> = vec![];
let mut error = StructError::Specific(Default::default());
Expand All @@ -188,105 +194,152 @@ impl StructAttrs {
.clone()
.parser();
while !parser.is_empty() {
let field = parser
.next_ident()
.ok_or_else(|| ErrorMessage::call_site(VALID_FORMAT))?;
match field.to_string().as_str() {
"ident" => {
parser
.next_tt_eq()
.ok_or_else(|| ErrorMessage::call_site(VALID_FORMAT))?;
ident = Some(
parser
.next_ident()
.ok_or_else(|| ErrorMessage::call_site(VALID_FORMAT))?,
)
}
"aliases" => {
parser
.next_tt_eq()
.ok_or_else(|| ErrorMessage::call_site(VALID_FORMAT))?;
let mut parser = parser
.next_bracketed()
.ok_or_else(|| ErrorMessage::call_site(VALID_FORMAT))?
.stream()
.parser();
aliases.extend(iter::from_fn(|| {
_ = parser.next_tt_comma();
parser.next_ident().map(|t| t.to_string())
}));
if !parser.is_empty() {
bail!("{VALID_FORMAT}")
eprintln!("{}", parser.to_token_stream());
if let Some(not) = parser.next_tt_not() {
if let Some(kw) = parser.next_keyword("ident") {
if let Some(ident_span) = ident_span {
bail!(
error_message!(ident_span, "ident is specified twice")
+ error_message!(
not.span()..kw.span(),
"ident was already specified"
)
)
} else {
ident_span = Some(not.span()..kw.span());
}
}
"error" => {
if parser.next_tt_eq().is_some() {
error = StructError::Generic(
FormatString::parse(parser)
} else {
let field = parser
.next_ident()
.ok_or_else(|| ErrorMessage::call_site(VALID_FORMAT))?;
match field.to_string().as_str() {
"ident" => {
parser
.next_tt_eq()
.ok_or_else(|| ErrorMessage::call_site(VALID_FORMAT))?;
ident = Some(
parser
.next_ident()
.ok_or_else(|| ErrorMessage::call_site(VALID_FORMAT))?,
);
} else {
let parser = &mut parser
.next_parenthesized()
if let Some(ident_span) = ident_span {
bail!(
error_message!(ident_span, "ident is specified twice")
+ error_message!(
field.span()..ident.unwrap().span(),
"ident was already specified"
)
)
}
}
"aliases" => {
parser
.next_tt_eq()
.ok_or_else(|| ErrorMessage::call_site(VALID_FORMAT))?;
let mut parser = parser
.next_bracketed()
.ok_or_else(|| ErrorMessage::call_site(VALID_FORMAT))?
.stream()
.parser();
let error = if let StructError::Specific(error) = &mut error {
error
aliases.extend(iter::from_fn(|| {
_ = parser.next_tt_comma();
parser.next_ident().map(|t| t.to_string())
}));
if !parser.is_empty() {
bail!("{VALID_FORMAT}")
}
}
"error" => {
if parser.next_tt_eq().is_some() {
error = StructError::Generic(
FormatString::parse(parser)
.ok_or_else(|| ErrorMessage::call_site(VALID_FORMAT))?,
);
} else {
error = StructError::Specific(Default::default());
if let StructError::Specific(error) = &mut error {
let parser = &mut parser
.next_parenthesized()
.ok_or_else(|| ErrorMessage::call_site(VALID_FORMAT))?
.stream()
.parser();
let error = if let StructError::Specific(error) = &mut error {
error
} else {
unreachable!()
}
};
while !parser.is_empty() {
let field = parser
.next_ident()
.ok_or_else(|| ErrorMessage::call_site(VALID_FORMAT))?;
let mut string = |f: &mut _| -> Result<()> {
parser
.next_tt_eq()
.ok_or_else(|| ErrorMessage::call_site(VALID_FORMAT))?;
*f = FormatString::parse(parser)
.ok_or_else(|| ErrorMessage::call_site(VALID_FORMAT))?;
Ok(())
};
match field.to_string().as_str() {
"unknown_field" => string(&mut error.unknown_field),
"unknown_field_empty" => string(&mut error.unknown_field_empty),
"unknown_field_single" => {
string(&mut error.unknown_field_single)
error = StructError::Specific(Default::default());
if let StructError::Specific(error) = &mut error {
error
} else {
unreachable!()
}
"duplicate_field" => string(&mut error.duplicate_field),
"missing_field" => string(&mut error.missing_field),
"field_help" => string(&mut error.field_help),
"conflict" => string(&mut error.conflict),
_ => bail!(field, "{VALID_FORMAT}"),
}?;
_ = parser.next_tt_comma();
};
while !parser.is_empty() {
let field = parser
.next_ident()
.ok_or_else(|| ErrorMessage::call_site(VALID_FORMAT))?;
let mut string = |f: &mut _| -> Result<()> {
parser
.next_tt_eq()
.ok_or_else(|| ErrorMessage::call_site(VALID_FORMAT))?;
*f = FormatString::parse(parser)
.ok_or_else(|| ErrorMessage::call_site(VALID_FORMAT))?;
Ok(())
};
match field.to_string().as_str() {
"unknown_field" => string(&mut error.unknown_field),
"unknown_field_empty" => {
string(&mut error.unknown_field_empty)
}
"unknown_field_single" => {
string(&mut error.unknown_field_single)
}
"duplicate_field" => string(&mut error.duplicate_field),
"missing_field" => string(&mut error.missing_field),
"field_help" => string(&mut error.field_help),
"conflict" => string(&mut error.conflict),
_ => bail!(field, "{VALID_FORMAT}"),
}?;
_ = parser.next_tt_comma();
}
}
}
// "duplicate" => {
// parser.next_eq() .ok_or_else(||
// ErrorMessage::call_site(VALID_FORMAT))?; let strategy
// = parser.next_ident() .ok_or_else(||
// ErrorMessage::call_site(VALID_FORMAT))?; duplicate =
// match strategy.to_string().as_str() {
// "AggregateOrError" => DuplicateStrategy::AggregateOrError,
// "Error" => DuplicateStrategy::Error,
// "AggregateOrOverride" => DuplicateStrategy::AggregateOrOverride,
// "Override" => DuplicateStrategy::Override,
// _ => abort!(strategy, VALID_FORMAT),
// }
// }
_ => bail!(field, "{VALID_FORMAT}"),
}
// "duplicate" => {
// parser.next_eq() .ok_or_else(||
// ErrorMessage::call_site(VALID_FORMAT))?; let strategy
// = parser.next_ident() .ok_or_else(||
// ErrorMessage::call_site(VALID_FORMAT))?; duplicate =
// match strategy.to_string().as_str() {
// "AggregateOrError" => DuplicateStrategy::AggregateOrError,
// "Error" => DuplicateStrategy::Error,
// "AggregateOrOverride" => DuplicateStrategy::AggregateOrOverride,
// "Override" => DuplicateStrategy::Override,
// _ => abort!(strategy, VALID_FORMAT),
// }
// }
_ => bail!(field, "{VALID_FORMAT}"),
}
_ = parser.next_tt_comma();
}
}

if ident_span.is_none() && ident.is_none() {
let ident_string = struct_ident.to_string();
let mut out = String::with_capacity(ident_string.len());
let mut ident_string = ident_string.chars();

out.extend(ident_string.next().into_iter().flat_map(char::to_lowercase));
for c in ident_string {
if c.is_uppercase() {
out.push('_');
out.extend(c.to_lowercase());
} else {
out.push(c);
}
}

ident = Some(Ident::new(&out, struct_ident.span()));
}

Ok(Self {
ident,
aliases,
Expand Down Expand Up @@ -790,11 +843,11 @@ pub fn from_attr_derive(
mut aliases,
error: ref struct_error,
// duplicate,
} = StructAttrs::from_attrs(attrs)?;
} = StructAttrs::from_attrs(&ident, attrs)?;

let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

if let Some(attribute_ident) = attribute_ident.clone() {
if let Some(ref attribute_ident) = attribute_ident {
aliases.insert(0, attribute_ident.to_string());
}

Expand All @@ -815,7 +868,7 @@ pub fn from_attr_derive(
fields: fields @ (Fields::Named(_) | Fields::Unnamed(_)),
..
}) => AttrField::parse_fields(fields, struct_error, attribute_ident)?,
_ => bail!("only works on structs with named fields"),
_ => bail!("only works on structs with fields"),
};

let conflicts = conflicts.to_tokens(struct_error)?;
Expand Down
17 changes: 7 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,23 +127,20 @@
//!
//! For helper attributes there is:
//! - [`FromAttr::from_attributes`] which takes in an [`IntoIterator<Item = &'a
//! syn::Attribute`](syn::Attribute)
//! (e.g. a [`&Vec<syn::Attribute>`](syn::Attribute)). Most useful for derive
//! macros.
//! syn::Attribute`](syn::Attribute) (e.g. a
//! [`&Vec<syn::Attribute>`](syn::Attribute)). Most useful for derive macros.
//! - [`FromAttr::remove_attributes`] which takes a [`&mut
//! Vec<syn::Attribute>`](syn::Attribute)
//! and does not only parse the attributes, but also removes those matching.
//! Useful for helper attributes for attribute macros, where the helper
//! attributes need to be removed.
//! Vec<syn::Attribute>`](syn::Attribute) and does not only parse the
//! attributes, but also removes those matching. Useful for helper attributes
//! for attribute macros, where the helper attributes need to be removed.
//!
//! For parsing a single [`TokenStream`] e.g. for parsing the proc macro input
//! there are two ways:
//!
//! - [`FromAttr::from_args`] taking in a [`TokenStream`]
//! - As `derive(FromAttr)` also derives [`Parse`] so you can use the
//! [parse](mod@syn::parse) API,
//! e.g. with [`parse_macro_input!(tokens as
//! Attribute)`](syn::parse_macro_input!).
//! [parse](mod@syn::parse) API, e.g. with [`parse_macro_input!(tokens as
//! Attribute)`](syn::parse_macro_input!).
//!
//! [interpolator]: https://docs.rs/interpolator/latest/interpolator/
use std::borrow::Borrow;
Expand Down
21 changes: 20 additions & 1 deletion tests/derive.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use attribute_derive::parsing::AttributeNamed;
use attribute_derive::{FlagOrValue, FromAttr};
use attribute_derive::{FlagOrValue, FromAttr, AttributeIdent};
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::{ParseStream, Parser};
Expand Down Expand Up @@ -271,3 +271,22 @@ fn attribute_ident_option() {
let attr: Attribute = parse_quote!(#[not_test("hello")]);
assert!(Option::<Test>::from_attributes([attr]).unwrap().is_none());
}

#[test]
#[allow(unused)]
fn attribute_ident_default() {
#[derive(FromAttr)]
#[attribute(ident = a)]
struct WithIdent(String);

assert_eq!(WithIdent::IDENTS, ["a"]);

#[derive(FromAttr)]
#[attribute(!ident)]
struct WithOutIdent(String);
static_assertions::assert_not_impl_any!(WithOutIdent: AttributeIdent);

#[derive(FromAttr)]
struct WithDefault(String);
assert_eq!(WithDefault::IDENTS, ["with_default"]);
}

0 comments on commit fc35f8f

Please sign in to comment.