Skip to content

Commit

Permalink
Add subenum-specific proc macros (#21)
Browse files Browse the repository at this point in the history
* Add subenum-specific proc macros

* Run cargo fmt and check

* Document feature
  • Loading branch information
Nexelous authored Sep 19, 2023
1 parent 1ffcd8f commit 59dfdf4
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 36 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
38 changes: 35 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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.
31 changes: 17 additions & 14 deletions src/build.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -97,14 +94,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::<Vec<TokenStream2>>();
let child_generics = self.generics.clone();

let child_ident = &self.ident;
let parent_ident = &parent.ident;
Expand Down Expand Up @@ -139,12 +138,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)*

Expand Down
7 changes: 6 additions & 1 deletion src/enum.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
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};

pub struct Enum {
pub ident: Ident,
pub variants: Punctuated<Variant, Token![,]>,
pub variants_attributes: Vec<Vec<TokenStream>>,
pub attributes: Vec<TokenStream>,
pub derives: Vec<Derive>,
pub generics: Generics,
}

impl Enum {
pub fn new(ident: Ident, derives: Vec<Derive>) -> Self {
pub fn new(ident: Ident, attributes: Vec<TokenStream>, derives: Vec<Derive>) -> Self {
Enum {
ident,
variants: Punctuated::new(),
variants_attributes: Vec::new(),
attributes,
derives,
generics: Generics {
lt_token: Some(syn::token::Lt::default()),
Expand Down
88 changes: 70 additions & 18 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -61,31 +64,49 @@ fn sanitize_input(input: &mut DeriveInput) {
}
}

fn attribute_paths(attr: &Attribute) -> impl Iterator<Item = Path> {
fn attribute_paths(attr: &Attribute) -> impl Iterator<Item = Meta> {
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<Ident, Enum> {
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::<Vec<proc_macro2::TokenStream>>(),
),
_ => 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()
}

Expand All @@ -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),
}
}
}
Expand All @@ -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::<Vec<proc_macro2::TokenStream>>(),
),
_ => unimplemented!("e"),
};

// We want all attributes except the "subenum" one.
var.attrs = var
.attrs
Expand All @@ -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);
}
}
}
Expand All @@ -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);

Expand Down
41 changes: 41 additions & 0 deletions tests/subenum_specific.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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::<Keyword>().unwrap();
let d = Token::from(c);
println!("{:?} {:?}", c, d);

assert_eq!(a, b);
}

#[subenum(EnumB)]
enum EnumA<T> {
#[subenum(EnumB)]
B,
#[subenum(EnumB)]
C(T),
}

0 comments on commit 59dfdf4

Please sign in to comment.