1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2
3use proc_macro::TokenStream;
4use proc_macro2::TokenStream as TokenStream2;
5use quote::quote;
6use std::collections::HashMap;
7
8#[proc_macro_derive(Into, attributes(into))]
10pub fn impl_into(input: TokenStream) -> TokenStream {
11 let syn::DeriveInput {
12 ident, data, attrs, ..
13 } = syn::parse_macro_input!(input as syn::DeriveInput);
14
15 let struct_fields = match &data {
16 syn::Data::Struct(st) => Some(&st.fields),
17 _ => None,
18 };
19
20 let global_attrs = read_attr_values(&attrs, struct_fields);
21 let has_skip = global_attrs.iter().any(|a| matches!(a, AttrValue::Skip));
22
23 let global_impls = global_attrs.into_iter().filter_map(|attr| match attr {
24 AttrValue::Custom { ty, expr } => Some(quote! {
25 impl ::std::convert::Into<#ty> for #ident {
26 fn into(self) -> #ty {
27 let value = self;
28 #expr
29 }
30 }
31 }),
32 AttrValue::Skip => None,
33 });
34
35 match &data {
36 syn::Data::Struct(st) => {
37 let mut struct_field_impls = Vec::new();
38
39 if st.fields.len() == 1 && !has_skip {
40 let field = st.fields.iter().next().unwrap();
41 let ty = &field.ty;
42 let body = match &field.ident {
43 Some(id) => quote! { self.#id },
44 None => quote! { self.0 },
45 };
46
47 struct_field_impls.push(quote! {
48 impl ::std::convert::Into<#ty> for #ident {
49 fn into(self) -> #ty {
50 #body
51 }
52 }
53 });
54 }
55
56 quote! {
57 #(#global_impls)*
58 #(#struct_field_impls)*
59 }
60 .into()
61 }
62
63 syn::Data::Enum(en) => {
64 let mut target_types: HashMap<String, (syn::Type, Vec<TokenStream2>)> = HashMap::new();
66
67 for variant in &en.variants {
68 let parsed_attrs = read_attr_values(&variant.attrs, Some(&variant.fields));
69
70 if parsed_attrs.iter().any(|a| matches!(a, AttrValue::Skip)) {
71 continue;
72 }
73
74 let var_ident = &variant.ident;
75
76 let match_pattern = match &variant.fields {
78 syn::Fields::Named(fields_named) => {
79 let idents = fields_named.named.iter().map(|f| &f.ident);
80 quote! { Self::#var_ident { #(#idents),* } }
81 }
82 syn::Fields::Unnamed(fields_unnamed) => {
83 let placeholders = (0..fields_unnamed.unnamed.len())
84 .map(|i| quote::format_ident!("__{}", i));
85 quote! { Self::#var_ident ( #(#placeholders),* ) }
86 }
87 syn::Fields::Unit => quote! { Self::#var_ident },
88 };
89
90 let binding = match &variant.fields {
92 syn::Fields::Named(fields_named) if fields_named.named.len() == 1 => {
93 let id = &fields_named.named[0].ident;
94 quote! { let value = #id; }
95 }
96 syn::Fields::Unnamed(fields_unnamed) if fields_unnamed.unnamed.len() == 1 => {
97 quote! { let value = __0; }
98 }
99 _ => quote! {},
100 };
101
102 if parsed_attrs.is_empty() && variant.fields.len() == 1 {
104 let field = variant.fields.iter().next().unwrap();
105 let ty = &field.ty;
106 let ty_string = quote! { #ty }.to_string();
107
108 let arm = quote! {
109 #match_pattern => {
110 #binding
111 value
112 }
113 };
114
115 target_types
116 .entry(ty_string)
117 .or_insert_with(|| (ty.clone(), Vec::new()))
118 .1
119 .push(arm);
120 } else {
121 for attr in parsed_attrs {
123 if let AttrValue::Custom { ty, expr } = attr {
124 let ty_string = quote! { #ty }.to_string();
125
126 let arm = quote! {
127 #match_pattern => {
128 #binding
129 #expr
130 }
131 };
132
133 target_types
134 .entry(ty_string)
135 .or_insert_with(|| (ty.clone(), Vec::new()))
136 .1
137 .push(arm);
138 }
139 }
140 }
141 }
142
143 let enum_impls = target_types.into_iter().map(|(_, (ty, arms))| {
145 quote! {
146 impl ::std::convert::Into<#ty> for #ident {
147 fn into(self) -> #ty {
148 match self {
149 #(#arms)*
150 _ => panic!("Incompatible enum variant for Into conversion"),
151 }
152 }
153 }
154 }
155 });
156
157 quote! {
158 #(#global_impls)*
159 #(#enum_impls)*
160 }
161 .into()
162 }
163
164 _ => panic!("Expected a 'struct' or 'enum'"),
165 }
166}
167
168enum AttrValue {
169 Custom { ty: syn::Type, expr: TokenStream2 },
170 Skip,
171}
172
173fn read_attr_values(attrs: &[syn::Attribute], _fields: Option<&syn::Fields>) -> Vec<AttrValue> {
174 attrs
175 .iter()
176 .filter(|attr| attr.path().is_ident("into"))
177 .map(|attr| match &attr.meta {
178 syn::Meta::List(list) => {
179 let args: IntoArgs = list.parse_args().expect("Invalid arguments");
180 match args {
181 IntoArgs::With(ty, path) => AttrValue::Custom {
182 ty: ty.clone(),
183 expr: quote! { #path(value) },
184 },
185 IntoArgs::Expr(ty, expr) => AttrValue::Custom {
186 ty: ty.clone(),
187 expr: quote! { #expr },
188 },
189 IntoArgs::Skip => AttrValue::Skip,
190 }
191 }
192 _ => panic!("Unsupported attribute format. Use #[into(skip)] or #[into(Type, ...)]"),
193 })
194 .collect()
195}
196
197enum IntoArgs {
198 With(syn::Type, syn::Path),
199 Expr(syn::Type, syn::Expr),
200 Skip,
201}
202
203impl syn::parse::Parse for IntoArgs {
204 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
205 if input.peek(syn::Ident) {
206 let fork = input.fork();
207 let ident: syn::Ident = fork.parse()?;
208 if ident == "skip" {
209 input.parse::<syn::Ident>()?;
210 return Ok(IntoArgs::Skip);
211 }
212 }
213
214 let ty: syn::Type = input.parse()?;
215 input.parse::<syn::token::Comma>()?;
216 let ident: syn::Ident = input.parse()?;
217 input.parse::<syn::token::Eq>()?;
218
219 if ident == "with" {
220 let path: syn::Path = input.parse()?;
221 Ok(IntoArgs::With(ty, path))
222 } else if ident == "expr" {
223 let expr: syn::Expr = input.parse()?;
224 Ok(IntoArgs::Expr(ty, expr))
225 } else {
226 Err(syn::Error::new(
227 ident.span(),
228 "expected `with`, `expr` or `skip`",
229 ))
230 }
231 }
232}