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 subenum-specific proc macros #21

Merged
merged 3 commits into from
Sep 19, 2023
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
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),
}