From 3d104065ed147ad2b0b09445a67a7b8a61d6962c Mon Sep 17 00:00:00 2001 From: Nexelous Date: Sun, 17 Sep 2023 19:33:34 +0300 Subject: [PATCH 1/3] Add subenum-specific proc macros --- Cargo.toml | 1 + src/build.rs | 26 +++++++++------ src/enum.rs | 7 +++- src/lib.rs | 88 ++++++++++++++++++++++++++++++++++++++++----------- tests/test.rs | 42 ++++++++++++++++++++++++ 5 files changed, 135 insertions(+), 29 deletions(-) create mode 100644 tests/test.rs diff --git a/Cargo.toml b/Cargo.toml index ec4891e..27934b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ proc-macro = true name = "subenum" [dev-dependencies] +derive_more = "0.99.17" strum = { version = "0.24.1", features = ["derive"], default-features = false } [dependencies] diff --git a/src/build.rs b/src/build.rs index 50d733b..055a174 100644 --- a/src/build.rs +++ b/src/build.rs @@ -97,14 +97,16 @@ impl Enum { } } - pub fn build(&self, parent: &DeriveInput, parent_data: &DataEnum) -> TokenStream2 { - let mut child_data = parent_data.clone(); - child_data.variants = self.variants.clone(); - - let mut child = parent.clone(); - child.ident = self.ident.clone(); - child.data = Data::Enum(child_data); - child.generics = self.generics.clone(); + pub fn build(&self, parent: &DeriveInput) -> TokenStream2 { + let attributes = self.attributes.clone(); + let child_attrs = parent.attrs.clone(); + let variants = self + .variants + .iter() + .zip(self.variants_attributes.clone()) + .map(|(variant, attribute)| quote! { #(#attribute)* #variant }) + .collect::>(); + let child_generics = self.generics.clone(); let child_ident = &self.ident; let parent_ident = &parent.ident; @@ -139,12 +141,16 @@ impl Enum { let vis = &parent.vis; - let (_child_impl, child_ty, _child_where) = child.generics.split_for_impl(); + let (_child_impl, child_ty, _child_where) = child_generics.split_for_impl(); let (parent_impl, parent_ty, parent_where) = parent.generics.split_for_impl(); quote!( - #child + #(#[ #attributes ])* + #(#child_attrs)* + #vis enum #child_ident #child_generics { + #(#variants),* + } #(#inherited_derives)* diff --git a/src/enum.rs b/src/enum.rs index f59a12f..c760417 100644 --- a/src/enum.rs +++ b/src/enum.rs @@ -1,6 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::vec::Vec; +use proc_macro2::TokenStream; use syn::{punctuated::Punctuated, Generics, Ident, Token, TypeParamBound, Variant}; use crate::{extractor::Extractor, iter::BoxedIter, param::Param, Derive}; @@ -8,15 +9,19 @@ use crate::{extractor::Extractor, iter::BoxedIter, param::Param, Derive}; pub struct Enum { pub ident: Ident, pub variants: Punctuated, + pub variants_attributes: Vec>, + pub attributes: Vec, pub derives: Vec, pub generics: Generics, } impl Enum { - pub fn new(ident: Ident, derives: Vec) -> Self { + pub fn new(ident: Ident, attributes: Vec, derives: Vec) -> Self { Enum { ident, variants: Punctuated::new(), + variants_attributes: Vec::new(), + attributes, derives, generics: Generics { lt_token: Some(syn::token::Lt::default()), diff --git a/src/lib.rs b/src/lib.rs index 64831c3..444622d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,17 +25,20 @@ use proc_macro2::Ident; use quote::quote; use r#enum::Enum; use syn::{ - parse_macro_input, Attribute, AttributeArgs, DeriveInput, Field, Meta, NestedMeta, Path, Type, + parse_macro_input, Attribute, AttributeArgs, DeriveInput, Field, Meta, MetaList, MetaNameValue, + NestedMeta, Type, }; const SUBENUM: &str = "subenum"; +const ERR: &str = + "subenum must be called with a list of identifiers, like `#[subenum(EnumA, EnumB(derive(Clone)))]`"; fn snake_case(field: &Field) -> Ident { let ident = field.ident.as_ref().unwrap_or_else(|| { // No ident; the Type must be Path. Use that. match &field.ty { Type::Path(path) => path.path.get_ident().unwrap(), - _ => unimplemented!(), + _ => unimplemented!("a"), } }); Ident::new(&ident.to_string().to_snake_case(), ident.span()) @@ -61,31 +64,49 @@ fn sanitize_input(input: &mut DeriveInput) { } } -fn attribute_paths(attr: &Attribute) -> impl Iterator { +fn attribute_paths(attr: &Attribute) -> impl Iterator { let meta = attr.parse_meta().unwrap(); let nested = match meta { Meta::List(list) => list.nested, - _ => unimplemented!(), + _ => unimplemented!("b"), }; nested.into_iter().map(|nested| match nested { - NestedMeta::Meta(Meta::Path(path)) => path, - _ => unimplemented!(), + NestedMeta::Meta(meta) => meta, + _ => unimplemented!("c"), }) } fn build_enum_map(args: AttributeArgs, derives: &[Derive]) -> BTreeMap { - let err = "subenum must be called with a list of identifiers, like `#[subenum(EnumA, EnumB)]`"; args.into_iter() .map(|nested| match nested { NestedMeta::Meta(meta) => meta, - NestedMeta::Lit(_) => panic!("{err}"), + NestedMeta::Lit(_) => panic!("{}", ERR), }) .map(|meta| match meta { - Meta::Path(path) => path, - _ => panic!("{err}"), + Meta::Path(path) => (path.get_ident().expect(ERR).to_owned(), Vec::new()), + Meta::List(MetaList { path, nested, .. }) => ( + path.get_ident().expect(ERR).to_owned(), + nested + .into_iter() + .map(|nested| match nested { + NestedMeta::Meta(meta) => meta, + NestedMeta::Lit(_) => panic!("{}", ERR), + }) + .map(|meta| match meta { + Meta::Path(path) => quote! { #path }, + Meta::List(MetaList { path, nested, .. }) => quote! { #path(#nested) }, + Meta::NameValue(MetaNameValue { path, lit, .. }) => quote! { #path = #lit }, + }) + .collect::>(), + ), + _ => panic!("{}", ERR), + }) + .map(|(ident, attrs)| { + ( + ident.clone(), + Enum::new(ident.clone(), attrs, derives.to_owned()), + ) }) - .map(|path| path.get_ident().expect(err).to_owned()) - .map(|ident| (ident.clone(), Enum::new(ident, derives.to_owned()))) .collect() } @@ -101,9 +122,14 @@ pub fn subenum(args: TokenStream, tokens: TokenStream) -> TokenStream { let mut derives = Vec::new(); for attr in &input.attrs { if attr.path.is_ident("derive") { - for path in attribute_paths(attr) { - if path.is_ident("PartialEq") { - derives.push(Derive::PartialEq); + for meta in attribute_paths(attr) { + match meta { + Meta::Path(path) => { + if path.is_ident("PartialEq") { + derives.push(Derive::PartialEq); + } + } + _ => unimplemented!("{:?}", meta), } } } @@ -114,10 +140,35 @@ pub fn subenum(args: TokenStream, tokens: TokenStream) -> TokenStream { for attribute in &variant.attrs { // Check for "subenum", iterate through the idents. if attribute.path.is_ident(SUBENUM) { - for path in attribute_paths(attribute) { - let ident = path.get_ident().unwrap(); + for meta in attribute_paths(attribute) { let mut var = variant.clone(); + let (ident, attrs) = match meta { + Meta::Path(ref path) => (path.get_ident().unwrap(), Vec::new()), + Meta::List(MetaList { + ref path, nested, .. + }) => ( + path.get_ident().unwrap(), + nested + .into_iter() + .map(|nested| match nested { + NestedMeta::Meta(meta) => meta, + NestedMeta::Lit(_) => panic!("{}", ERR), + }) + .map(|meta| match meta { + Meta::Path(path) => quote! { #[ #path ] }, + Meta::List(MetaList { path, nested, .. }) => { + quote! { #[ #path(#nested) ] } + } + Meta::NameValue(MetaNameValue { path, lit, .. }) => { + quote! { #[ #path = #lit ] } + } + }) + .collect::>(), + ), + _ => unimplemented!("e"), + }; + // We want all attributes except the "subenum" one. var.attrs = var .attrs @@ -130,6 +181,7 @@ pub fn subenum(args: TokenStream, tokens: TokenStream) -> TokenStream { .get_mut(ident) .expect("All enums to be created must be declared at the top-level subenum attribute"); e.variants.push(var); + e.variants_attributes.push(attrs); } } } @@ -139,7 +191,7 @@ pub fn subenum(args: TokenStream, tokens: TokenStream) -> TokenStream { e.compute_generics(&input.generics); } - let enums: Vec<_> = enums.into_values().map(|e| e.build(&input, data)).collect(); + let enums: Vec<_> = enums.into_values().map(|e| e.build(&input)).collect(); sanitize_input(&mut input); diff --git a/tests/test.rs b/tests/test.rs new file mode 100644 index 0000000..6a948e6 --- /dev/null +++ b/tests/test.rs @@ -0,0 +1,42 @@ +use subenum::subenum; + +#[subenum( + Binary(derive(derive_more::Display)), + Unary, + Keyword(derive(Copy, strum::EnumString), strum(serialize_all = "snake_case")) +)] +#[derive(Clone, Debug, PartialEq)] +enum Token { + #[subenum(Binary(display(fmt = "-")), Unary)] + Minus, + #[subenum(Binary(display(fmt = "+")))] + Plus, + #[subenum(Keyword)] + And, + #[subenum(Keyword)] + Or, + #[subenum(Keyword)] + Var, +} + +#[test] +fn test_token() { + let a = Token::Minus; + let b = Binary::try_from(a.clone()).unwrap(); + println!("b: {}", b); + + let c = "and".parse::().unwrap(); + let d = Token::from(c); + println!("{:?} {:?}", c, d); + + assert_eq!(a, b); +} + +#[subenum(EnumB)] +enum EnumA { + A, + #[subenum(EnumB)] + B, + #[subenum(EnumB)] + C(T) +} \ No newline at end of file From 445e51359e69f1894e801f174f04826b3ddd73f1 Mon Sep 17 00:00:00 2001 From: Nexelous Date: Mon, 18 Sep 2023 21:47:01 +0300 Subject: [PATCH 2/3] Run cargo fmt and check --- tests/test.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test.rs b/tests/test.rs index 6a948e6..5826ed7 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -34,9 +34,8 @@ fn test_token() { #[subenum(EnumB)] enum EnumA { - A, #[subenum(EnumB)] B, #[subenum(EnumB)] - C(T) -} \ No newline at end of file + C(T), +} From 77d3a50c30f217fb052c80cd9c45fde341492194 Mon Sep 17 00:00:00 2001 From: Paho Lurie-Gregg Date: Mon, 18 Sep 2023 19:11:49 -0700 Subject: [PATCH 3/3] Document feature --- CHANGELOG.md | 1 + README.md | 38 ++++++++++++++++++++++++-- src/build.rs | 5 +--- tests/{test.rs => subenum_specific.rs} | 0 4 files changed, 37 insertions(+), 7 deletions(-) rename tests/{test.rs => subenum_specific.rs} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0982e46..adb070a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ This project follows semantic versioning. ### Unreleased - [added] Default feature `std` and support for no-std. +- [added] Support for subenum-specific proc-macros. ### 1.0.1 (2023-02-25) - [fixed] References to generic types. diff --git a/README.md b/README.md index adeebf7..e3749c3 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ fn main() -> Result<(), EdibleConvertError> { ## Complex Example -In addition to simple enums and built-in traits, `subenum` works with complex enums and third-party attributes. +In addition to simple enums and built-in traits, `subenum` works with complex +enums and third-party attributes. ```rust use subenum::subenum; @@ -107,6 +108,33 @@ fn main() -> Result<(), TreeConvertError> { } ``` +## Subenum-specific proc-macros + +Maybe you have an enum that can't be `Copy`d, but the subenum can, and you want +to derive it: + +```rust +use subenum::subenum; + +#[subenum(Bar, Qux(derive(Copy)))] +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Foo { + #[subenum(Bar)] + A(String), + #[subenum(Qux)] + B, + #[subenum(Bar, Qux)] + C(u8), +} + +fn main() { + let b = Qux::B; + let c = b; + assert_eq!(b, c); +} +``` + + # Limitations Bound lifetimes (e.g. `for<'a, 'b, 'c>`) are not currently supported. Please @@ -115,6 +143,10 @@ open a ticket if these are desired. # Features - `default` - `std` and `error_trait` - `std` - Use standard library collections and allocators within this proc macro -- `error_trait` - Implement [`Error`](https://doc.rust-lang.org/std/error/trait.Error.html) for `ConvertError` types. - - When combined with nightly and [`#![feature(error_in_core)]`](https://github.com/rust-lang/rust/issues/103765) supports `#[no_std]` +- `error_trait` - Implement + [`Error`](https://doc.rust-lang.org/std/error/trait.Error.html) for + `ConvertError` types. + - When combined with nightly and + [`#![feature(error_in_core)]`](https://github.com/rust-lang/rust/issues/103765) + supports `#[no_std]` - Otherwise, this feature requires `std` as well. diff --git a/src/build.rs b/src/build.rs index 055a174..e6ea78f 100644 --- a/src/build.rs +++ b/src/build.rs @@ -1,10 +1,7 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; use std::vec::Vec; -use syn::{ - punctuated::Punctuated, Data, DataEnum, DeriveInput, Generics, Ident, Token, TypeParamBound, - Variant, -}; +use syn::{punctuated::Punctuated, DeriveInput, Generics, Ident, Token, TypeParamBound, Variant}; use crate::{ derive::{partial_eq::partial_eq_arm, Derive}, diff --git a/tests/test.rs b/tests/subenum_specific.rs similarity index 100% rename from tests/test.rs rename to tests/subenum_specific.rs