diff --git a/CHANGELOG.md b/CHANGELOG.md index 42c52ff..b370fa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` ## [0.9.2] - 2024-06-22 diff --git a/Cargo.toml b/Cargo.toml index cb166f1..4febdc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" @@ -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] diff --git a/macro/src/lib.rs b/macro/src/lib.rs index 47891a7..26b34a8 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -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}; @@ -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"; @@ -170,8 +171,13 @@ struct StructAttrs { } impl StructAttrs { - fn from_attrs(attrs: impl IntoIterator) -> Result { - 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, + ) -> Result { + 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> = None; let mut ident: Option = None; let mut aliases: Vec = vec![]; let mut error = StructError::Specific(Default::default()); @@ -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, @@ -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()); } @@ -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)?; diff --git a/src/lib.rs b/src/lib.rs index 8743dc3..bc2a7e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,23 +127,20 @@ //! //! For helper attributes there is: //! - [`FromAttr::from_attributes`] which takes in an [`IntoIterator`](syn::Attribute)). Most useful for derive -//! macros. +//! syn::Attribute`](syn::Attribute) (e.g. a +//! [`&Vec`](syn::Attribute)). Most useful for derive macros. //! - [`FromAttr::remove_attributes`] which takes a [`&mut -//! Vec`](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) 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; diff --git a/tests/derive.rs b/tests/derive.rs index ef4d828..3aa1c95 100644 --- a/tests/derive.rs +++ b/tests/derive.rs @@ -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}; @@ -271,3 +271,22 @@ fn attribute_ident_option() { let attr: Attribute = parse_quote!(#[not_test("hello")]); assert!(Option::::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"]); +}