Skip to content

Commit 774e233

Browse files
committed
Add code from playground
0 parents  commit 774e233

12 files changed

+571
-0
lines changed

Cargo.lock

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "nested_enum_utils"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
proc-macro-crate = "3.1.0"
8+
proc-macro2 = "1.0"
9+
quote = "1.0"
10+
syn = { version = "1.0", features = ["full"] }
11+
12+
[lib]
13+
proc-macro = true
14+
15+
[dev-dependencies]
16+
trybuild = "1.0.96"

src/lib.rs

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
use std::collections::BTreeSet;
2+
3+
use proc_macro::TokenStream;
4+
use proc_macro2::TokenStream as TokenStream2;
5+
use quote::{quote, ToTokens};
6+
use syn::{
7+
parse::{Parse, ParseStream},
8+
parse_macro_input,
9+
punctuated::Punctuated,
10+
Data, DeriveInput, Fields, Ident, Token, Type, Variant,
11+
};
12+
13+
fn extract_enum_variants(input: &DeriveInput) -> syn::Result<Vec<(&syn::Ident, &syn::Type)>> {
14+
let mut distinct_types = BTreeSet::new();
15+
let Data::Enum(data_enum) = &input.data else {
16+
return Err(syn::Error::new_spanned(
17+
input,
18+
"EnumConversions can only be used with enums",
19+
));
20+
};
21+
data_enum.variants.iter().map(|variant: &Variant| {
22+
let variant_name = &variant.ident;
23+
match &variant.fields {
24+
Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
25+
let field_type = &fields.unnamed.first().unwrap().ty;
26+
if !distinct_types.insert(field_type.to_token_stream().to_string()) {
27+
return Err(syn::Error::new_spanned(
28+
field_type,
29+
"EnumConversions only works with enums that have unnamed single fields of distinct types"
30+
));
31+
}
32+
Ok((variant_name, field_type))
33+
},
34+
_ => Err(syn::Error::new_spanned(
35+
variant,
36+
"EnumConversions only works with enums that have unnamed single fields"
37+
))
38+
}
39+
}).collect()
40+
}
41+
42+
fn generate_enum_self_conversions(enum_name: &Ident, variants: &[(&Ident, &Type)]) -> TokenStream2 {
43+
let mut conversions = TokenStream2::new();
44+
45+
for (variant_name, field_type) in variants {
46+
// Generate From<FieldType> for Enum
47+
let from_impl = quote! {
48+
impl From<#field_type> for #enum_name {
49+
fn from(value: #field_type) -> Self {
50+
#enum_name::#variant_name(value)
51+
}
52+
}
53+
};
54+
conversions.extend(from_impl);
55+
56+
// Generate TryFrom<Enum> for FieldType
57+
//
58+
// This is a self conversion, so it case it does not work we want to return the original value
59+
let try_from_impl = quote! {
60+
impl TryFrom<#enum_name> for #field_type {
61+
type Error = #enum_name;
62+
63+
fn try_from(value: #enum_name) -> Result<Self, Self::Error> {
64+
match value {
65+
#enum_name::#variant_name(inner) => Ok(inner),
66+
x => Err(x),
67+
}
68+
}
69+
}
70+
};
71+
conversions.extend(try_from_impl);
72+
73+
// Generate TryFrom<Enum> for &FieldType
74+
let try_from_ref_impl = quote! {
75+
impl<'a> TryFrom<&'a #enum_name> for &'a #field_type {
76+
type Error = &'a #enum_name;
77+
78+
fn try_from(value: &'a #enum_name) -> Result<Self, Self::Error> {
79+
match value {
80+
#enum_name::#variant_name(inner) => Ok(inner),
81+
_ => Err(value),
82+
}
83+
}
84+
}
85+
};
86+
conversions.extend(try_from_ref_impl);
87+
}
88+
89+
conversions
90+
}
91+
92+
fn generate_enum_target_conversions(
93+
enum_name: &Ident,
94+
target_type: &Type,
95+
variants: &[(&Ident, &Type)],
96+
) -> TokenStream2 {
97+
let mut conversions = TokenStream2::new();
98+
99+
for (variant_name, field_type) in variants {
100+
// Generate From<FieldType> for TargetType
101+
let from_impl = quote! {
102+
impl From<#field_type> for #target_type {
103+
fn from(value: #field_type) -> Self {
104+
let enum_value = #enum_name::#variant_name(value);
105+
Self::from(enum_value)
106+
}
107+
}
108+
};
109+
conversions.extend(from_impl);
110+
111+
// Generate TryFrom<TargetType> for FieldType
112+
//
113+
// This is a self conversion, so it case it does not work we want to return the original value
114+
let try_from_impl = quote! {
115+
impl TryFrom<#target_type> for #field_type {
116+
type Error = #target_type;
117+
118+
fn try_from(value: #target_type) -> Result<Self, Self::Error> {
119+
match #enum_name::try_from(value) {
120+
Ok(#enum_name::#variant_name(inner)) => Ok(inner),
121+
Ok(x) => Err(#target_type::from(x)),
122+
Err(x) => Err(x),
123+
}
124+
}
125+
}
126+
};
127+
conversions.extend(try_from_impl);
128+
129+
// Generate TryFrom<&TargetType> for &FieldType
130+
//
131+
// This is a self conversion, so it case it does not work we want to return the original value
132+
let try_from_ref_impl = quote! {
133+
impl<'a> TryFrom<&'a #target_type> for &'a #field_type {
134+
type Error = &'a #target_type;
135+
136+
fn try_from(value: &'a #target_type) -> Result<Self, Self::Error> {
137+
match <&'a #enum_name>::try_from(value) {
138+
Ok(#enum_name::#variant_name(inner)) => Ok(inner),
139+
Ok(_) => Err(value),
140+
Err(_) => Err(value),
141+
}
142+
}
143+
}
144+
};
145+
conversions.extend(try_from_ref_impl);
146+
}
147+
148+
conversions
149+
}
150+
151+
struct EnumConversionsArgs {
152+
target_types: Punctuated<Type, Token![,]>,
153+
}
154+
155+
impl Parse for EnumConversionsArgs {
156+
fn parse(input: ParseStream) -> syn::Result<Self> {
157+
Ok(EnumConversionsArgs {
158+
target_types: Punctuated::parse_terminated(input)?,
159+
})
160+
}
161+
}
162+
163+
#[proc_macro_attribute]
164+
pub fn enum_conversions(attr: TokenStream, item: TokenStream) -> TokenStream {
165+
let args = parse_macro_input!(attr as EnumConversionsArgs);
166+
let input = parse_macro_input!(item as DeriveInput);
167+
168+
let enum_name = &input.ident;
169+
170+
let variants = match extract_enum_variants(&input) {
171+
Ok(v) => v,
172+
Err(e) => return e.to_compile_error().into(),
173+
};
174+
175+
let mut all_conversions = TokenStream2::new();
176+
177+
// Generate self-conversions
178+
all_conversions.extend(generate_enum_self_conversions(enum_name, &variants));
179+
180+
// Generate conversions for each target type
181+
for target_type in args.target_types {
182+
all_conversions.extend(generate_enum_target_conversions(
183+
enum_name,
184+
&target_type,
185+
&variants,
186+
));
187+
}
188+
189+
let expanded = quote! {
190+
#input
191+
#all_conversions
192+
};
193+
TokenStream::from(expanded)
194+
}
195+
196+
#[proc_macro_derive(EnumConversions)]
197+
pub fn derive_enum_conversions(input: TokenStream) -> TokenStream {
198+
let input = parse_macro_input!(input as DeriveInput);
199+
let enum_name = &input.ident;
200+
201+
let variants = match extract_enum_variants(&input) {
202+
Ok(v) => v,
203+
Err(e) => return e.to_compile_error().into(),
204+
};
205+
206+
let self_conversions = generate_enum_self_conversions(enum_name, &variants);
207+
208+
let expanded = quote! {
209+
#input
210+
#self_conversions
211+
};
212+
213+
TokenStream::from(expanded)
214+
}

tests/basic.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
use nested_enum_utils::enum_conversions;
2+
3+
#[test]
4+
fn test_single_enum() {
5+
#[derive(Debug)]
6+
#[enum_conversions]
7+
enum Test {
8+
A(u32),
9+
B(String),
10+
}
11+
12+
// convert from leaf to enum
13+
let e: Test = 42u32.into();
14+
// convert from enum to leaf by reference
15+
let lr: &u32 = (&e).try_into().unwrap();
16+
assert_eq!(*lr, 42);
17+
// convert from enum to leaf by value
18+
let l: u32 = e.try_into().unwrap();
19+
assert_eq!(l, 42);
20+
}
21+
22+
#[test]
23+
fn test_nested_enums() {
24+
#[derive(Debug)]
25+
#[enum_conversions(Outer)]
26+
enum Inner {
27+
A(u32),
28+
B(u8),
29+
}
30+
31+
#[derive(Debug)]
32+
#[enum_conversions]
33+
enum Outer {
34+
A(Inner),
35+
B(String),
36+
}
37+
38+
// convert from leaf to outer
39+
let e: Outer = 42u32.into();
40+
// convert from outer to leaf by reference
41+
let lr: &u32 = (&e).try_into().unwrap();
42+
assert_eq!(*lr, 42);
43+
// convert from outer to leaf by value
44+
let l: u32 = e.try_into().unwrap();
45+
assert_eq!(l, 42);
46+
}
47+
48+
#[test]
49+
fn test_deeply_nested_enums() {
50+
#[derive(Debug)]
51+
#[enum_conversions(Outer)]
52+
enum Inner {
53+
A(u32),
54+
B(u8),
55+
}
56+
57+
#[derive(Debug)]
58+
#[enum_conversions(Outer)]
59+
enum Mid {
60+
A(Inner),
61+
B(String),
62+
}
63+
64+
#[derive(Debug)]
65+
#[enum_conversions]
66+
enum Outer {
67+
A(Mid),
68+
B(f32),
69+
}
70+
71+
// convert from leaf to outer
72+
let e: Outer = 42u32.into();
73+
// convert from outer to leaf by reference
74+
let lr: &u32 = (&e).try_into().unwrap();
75+
assert_eq!(*lr, 42);
76+
// convert from outer to leaf by value
77+
let l: u32 = e.try_into().unwrap();
78+
assert_eq!(l, 42);
79+
}
80+
81+
#[test]
82+
fn compile_fail() {
83+
let t = trybuild::TestCases::new();
84+
t.compile_fail("tests/compile_fail/*.rs");
85+
}

tests/compile_fail/duplicate_type.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use nested_enum_utils::enum_conversions;
2+
3+
#[derive(Debug)]
4+
#[enum_conversions(Outer)]
5+
enum Enum {
6+
A(u8),
7+
B(u8),
8+
}
9+
10+
fn main() {}

0 commit comments

Comments
 (0)