Skip to content

Commit

Permalink
Implement enum_from_functions macro
Browse files Browse the repository at this point in the history
  • Loading branch information
speelbarrow committed Jul 25, 2023
1 parent 2428bb2 commit ebbd866
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 12 deletions.
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ edition = "2021"

[lib]
proc-macro = true

[dependencies]
convert_case = "0.6.0"
quote = "1.0.32"
syn = { version = "2.0.27", features = ["full", "extra-traits"] }
169 changes: 157 additions & 12 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ impl Enum {
}
}
# fn main() {
# assert_eq!(Enum::map(&Enum::Foo), "Foo");
# assert_eq!(Enum::map(&Enum::Bar), "Bar");
# assert_eq!(Enum::map(&Enum::Baz), "Baz");
# assert_eq!(Enum::map(Enum::Foo), "Foo");
# assert_eq!(Enum::map(Enum::Bar), "Bar");
# assert_eq!(Enum::map(Enum::Baz), "Baz");
# }
```
expands to:
Expand Down Expand Up @@ -55,7 +55,42 @@ impl Enum {
```
The signatures of all the functions in the `impl` block must be the same and must not use the `self` keyword. Aside
from that, any function signature will work with this macro.
```compile_fail
# use enum_from_functions::enum_from_functions;
#[enum_from_functions]
impl Enum {
// Causes a compile error because the `self` argument isn't allowed.
fn foo(self) -> &'static str {
"Foo"
}
}
```
```compile_fail
# use enum_from_functions::enum_from_functions;
// Causes a compile error because the return types don't match.
#[enum_from_functions]
impl Enum {
fn foo() -> &'static str {
"Foo"
}
fn bar() -> String {
"Bar".to_owned()
}
}
```
```compile_fail
# use enum_from_functions::enum_from_functions;
// Causes a compile error because the argument types don't match.
#[enum_from_functions]
impl Enum {
fn foo(_: i32) -> &'static str {
"Foo"
}
fn bar(_: bool) -> &'static str {
"Bar"
}
}
```
If you need to export the generated `enum` type out of its parent module, provide the `pub` argument to the macro
attribute.
```
Expand All @@ -69,9 +104,10 @@ mod internal {
}
}
use internal::Visible;
fn main() {
# assert!((|| { return
Visible::map(&Visible::Example);
Visible::map(Visible::Example);
# })());
}
```
Expand All @@ -86,7 +122,7 @@ fn main() {
# }
#
# fn main() {
# assert!(!NotVisible::map(&NotVisible::Example));
# assert!(!NotVisible::map(NotVisible::Example));
# }
```
Items in the `impl` block that are not functions will be ignored and passed through to the output unchanged.
Expand All @@ -113,21 +149,130 @@ impl Enum {
}
}
# fn main() {
# assert_eq!(Enum::map(&Enum::Foo), "Foo");
# assert_eq!(Enum::map(&Enum::Bar), "Bar");
# assert_eq!(Enum::map(&Enum::Baz), "Baz");
# assert_eq!(Enum::map(Enum::Foo), "Foo");
# assert_eq!(Enum::map(Enum::Bar), "Bar");
# assert_eq!(Enum::map(Enum::Baz), "Baz");
# let _ = format!("{:?}", Enum::Foo);
# }
```
*/

use proc_macro::TokenStream;
use convert_case::{Case, Casing};
use proc_macro::{Span, TokenStream};
use syn::{
parse_macro_input,
punctuated::{Pair, Punctuated},
token::Comma,
FnArg, ImplItem, Pat,
};

/**
A procedural macro attribute that generates an `enum` based on the functions defined in the `impl` block it annotates.
See the crate documentation for more information.
*/
#[proc_macro_attribute]
pub fn enum_from_functions(_args: TokenStream, input: TokenStream) -> TokenStream {
input
pub fn enum_from_functions(args: TokenStream, input: TokenStream) -> TokenStream {
// Parse the arguments either as empty or as a `pub` token. Any other arguments cause an error.
let parsed_pub = if !args.is_empty() {
Some(parse_macro_input!(args as syn::Token![pub]))
} else {
None
};

// Parse the input as an `impl` block (any other input will cause an error here).
let mut parsed_impl = parse_macro_input!(input as syn::ItemImpl);

// Set aside the attributes (if any) on the `impl` block for later, moving them out of the `impl` block.
let attrs = parsed_impl.attrs.drain(..).collect::<Vec<_>>();

// Iterate through the items in the `impl` block, looking for functions.
// Each function has its signature verified against the first found function. Then the name is converted to
// PascalCase and added to the list of variant identifiers.

let mut variants = Vec::<syn::Ident>::new();
let mut function_names = Vec::<syn::Ident>::new();
let mut first_sig: Option<&syn::Signature> = None;

for item in parsed_impl.items.iter() {
// Only proceed if the item is a function.
if let ImplItem::Fn(function) = item {
// If `first_sig` has already been set, verify this function's signature against it. Otherwise, assign it.
if let Some(first_sig) = first_sig {
macro_rules! anonimize {
($sig:expr) => {{
let mut to_anon = $sig.clone();
to_anon.ident =
syn::Ident::new("anon", proc_macro::Span::call_site().into());
to_anon
}};
}

let (anon_first_sig, anon_func_sig) =
(anonimize!(first_sig), anonimize!(&function.sig));
if anon_first_sig != anon_func_sig {
syn::Error::new(
Span::call_site().into(),
format!(
"mismatched signatures:\n\t`{:?}`\nand\n\t`{:?}`",
anon_first_sig, anon_func_sig
),
)
.into_compile_error();
}
} else {
// If the first function has a `self` argument, error out.
if let Some(syn::FnArg::Receiver(_)) = function.sig.inputs.first() {
syn::Error::new(
Span::call_site().into(),
"the `self` argument is not allowed in functions used by `enum_from_functions`",
)
.into_compile_error();
}

first_sig = Some(&function.sig);
}

// Convert the function's name to PascalCase and add it to the list of variant identifiers.
variants.push(syn::Ident::new(
&function.sig.ident.to_string().to_case(Case::Pascal),
Span::call_site().into(),
));
function_names.push(function.sig.ident.clone());
}
}

let enum_name = &parsed_impl.self_ty;
let (map_sig, arg_names) = first_sig.map_or((None, None), |some| {
let mut r_sig = some.clone();
r_sig.ident = syn::Ident::new("map", Span::call_site().into());
r_sig.inputs.insert(0, syn::parse_quote!(self));

let r_args = Punctuated::<&Box<Pat>, Comma>::from_iter(some.inputs.pairs().map(|pair| {
match pair.value() {
FnArg::Typed(arg) => Pair::new(&arg.pat, pair.punct().map(|_| Comma::default())),
FnArg::Receiver(_) => unreachable!(),
}
}));

(Some(r_sig), Some(r_args))
});
let out = quote::quote! {
#(#attrs)*
#parsed_pub enum #enum_name {
#(#variants),*
}

#parsed_impl

impl #enum_name {
pub #map_sig {
match self {
#(Self::#variants => Self::#function_names (#arg_names)),*
}
}
}
};

dbg!(&out.to_string());
out.into()
}

0 comments on commit ebbd866

Please sign in to comment.