Skip to content

Commit 83d7694

Browse files
committed
Support for custom error type in TryInto derive
1 parent e0a0713 commit 83d7694

File tree

3 files changed

+139
-11
lines changed

3 files changed

+139
-11
lines changed

impl/src/try_into.rs

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,50 @@ use crate::utils::{
22
add_extra_generic_param, numbered_vars, replace_self::DeriveInputExt as _,
33
AttrParams, DeriveType, MultiFieldData, State,
44
};
5-
use proc_macro2::TokenStream;
5+
use proc_macro2::{Span, TokenStream};
66
use quote::{quote, ToTokens};
7-
use syn::{DeriveInput, Result};
7+
use syn::{Attribute, DeriveInput, Ident, Result};
88

9-
use crate::utils::HashMap;
9+
use crate::utils::{
10+
attr::{self, ParseMultiple as _},
11+
HashMap, Spanning,
12+
};
1013

1114
/// Provides the hook to expand `#[derive(TryInto)]` into an implementation of `TryInto`
1215
#[allow(clippy::cognitive_complexity)]
1316
pub fn expand(input: &DeriveInput, trait_name: &'static str) -> Result<TokenStream> {
14-
let input = &input.replace_self_type();
17+
let input = &mut input.replace_self_type();
18+
19+
let trait_attr = "try_into";
20+
21+
let error_attrs: Vec<(usize, Attribute)> = input
22+
.attrs
23+
.iter()
24+
.enumerate()
25+
.filter(|(_, attr)| attr.path().is_ident(trait_attr))
26+
.filter(|(_, attr)| attr.parse_args_with(detect_error_attr).is_ok())
27+
.map(|(i, attr)| (i, attr.clone()))
28+
.collect();
29+
30+
for (i, _) in &error_attrs {
31+
let _ = &mut input.attrs.remove(*i);
32+
}
33+
34+
let error_attrs = error_attrs
35+
.into_iter()
36+
.map(|(_, attr)| attr)
37+
.collect::<Vec<Attribute>>();
38+
39+
let custom_error = attr::Error::parse_attrs(
40+
error_attrs,
41+
&Ident::new(trait_attr, Span::call_site()),
42+
)?
43+
.map(Spanning::into_inner);
1544

1645
let state = State::with_attr_params(
1746
input,
1847
trait_name,
19-
"try_into".into(),
48+
trait_attr.into(),
2049
AttrParams {
2150
enum_: vec!["ignore", "owned", "ref", "ref_mut"],
2251
variant: vec!["ignore", "owned", "ref", "ref_mut"],
@@ -101,26 +130,36 @@ pub fn expand(input: &DeriveInput, trait_name: &'static str) -> Result<TokenStre
101130
input.generics.split_for_impl()
102131
};
103132

104-
let error = quote! {
133+
let mut error_ty = quote! {
105134
derive_more::TryIntoError<#reference_with_lifetime #input_type #ty_generics>
106135
};
107136

137+
let mut error_conv = quote! {};
138+
139+
if let Some(custom_error) = custom_error.as_ref() {
140+
error_ty = custom_error.ty.to_token_stream();
141+
error_conv = custom_error.conv.as_ref().map_or_else(
142+
|| quote! { .map_err(derive_more::core::convert::Into::into) },
143+
|conv| quote! { .map_err(#conv) },
144+
);
145+
}
146+
108147
let try_from = quote! {
109148
#[automatically_derived]
110149
impl #impl_generics derive_more::core::convert::TryFrom<
111150
#reference_with_lifetime #input_type #ty_generics
112151
> for (#(#reference_with_lifetime #original_types),*) #where_clause {
113-
type Error = #error;
152+
type Error = #error_ty;
114153

115154
#[inline]
116155
fn try_from(
117156
value: #reference_with_lifetime #input_type #ty_generics,
118-
) -> derive_more::core::result::Result<Self, #error> {
157+
) -> derive_more::core::result::Result<Self, #error_ty> {
119158
match value {
120159
#(#matchers)|* => derive_more::core::result::Result::Ok(#vars),
121160
_ => derive_more::core::result::Result::Err(
122161
derive_more::TryIntoError::new(value, #variant_names, #output_type),
123-
),
162+
)#error_conv,
124163
}
125164
}
126165
}
@@ -129,3 +168,17 @@ pub fn expand(input: &DeriveInput, trait_name: &'static str) -> Result<TokenStre
129168
}
130169
Ok(tokens)
131170
}
171+
172+
fn detect_error_attr(input: syn::parse::ParseStream) -> Result<()> {
173+
mod ident {
174+
syn::custom_keyword!(error);
175+
}
176+
177+
let ahead = input.lookahead1();
178+
if ahead.peek(ident::error) {
179+
let _ = input.parse::<TokenStream>();
180+
Ok(())
181+
} else {
182+
Err(ahead.error())
183+
}
184+
}

impl/src/utils.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use syn::{
2121
feature = "from_str",
2222
feature = "into",
2323
feature = "try_from",
24+
feature = "try_into",
2425
))]
2526
pub(crate) use self::either::Either;
2627
#[cfg(any(feature = "from", feature = "into"))]
@@ -36,6 +37,7 @@ pub(crate) use self::generics_search::GenericsSearch;
3637
feature = "from_str",
3738
feature = "into",
3839
feature = "try_from",
40+
feature = "try_into",
3941
))]
4042
pub(crate) use self::spanning::Spanning;
4143

@@ -1324,6 +1326,7 @@ pub fn is_type_parameter_used_in_type(
13241326
feature = "from_str",
13251327
feature = "into",
13261328
feature = "try_from",
1329+
feature = "try_into",
13271330
))]
13281331
mod either {
13291332
use proc_macro2::TokenStream;
@@ -1397,6 +1400,7 @@ mod either {
13971400
feature = "from_str",
13981401
feature = "into",
13991402
feature = "try_from",
1403+
feature = "try_into",
14001404
))]
14011405
mod spanning {
14021406
use std::ops::{Deref, DerefMut};
@@ -1494,6 +1498,7 @@ mod spanning {
14941498
feature = "from_str",
14951499
feature = "into",
14961500
feature = "try_from",
1501+
feature = "try_into",
14971502
))]
14981503
pub(crate) mod attr {
14991504
use std::any::Any;
@@ -1512,7 +1517,7 @@ pub(crate) mod attr {
15121517
feature = "try_from"
15131518
))]
15141519
pub(crate) use self::empty::Empty;
1515-
#[cfg(feature = "from_str")]
1520+
#[cfg(any(feature = "from_str", feature = "try_into"))]
15161521
pub(crate) use self::error::Error;
15171522
#[cfg(any(feature = "display", feature = "from_str"))]
15181523
pub(crate) use self::rename_all::RenameAll;
@@ -2117,7 +2122,7 @@ pub(crate) mod attr {
21172122
}
21182123
}
21192124

2120-
#[cfg(feature = "from_str")]
2125+
#[cfg(any(feature = "from_str", feature = "try_into"))]
21212126
pub(crate) mod error {
21222127
use syn::parse::{Parse, ParseStream};
21232128

tests/try_into.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,3 +260,73 @@ fn test_try_into() {
260260
);
261261
assert!(matches!(i.try_into().unwrap(), ()));
262262
}
263+
264+
mod error {
265+
use core::fmt;
266+
267+
#[cfg(not(feature = "std"))]
268+
use alloc::string::String;
269+
270+
use derive_more::TryIntoError;
271+
272+
use super::*;
273+
274+
struct CustomError(String);
275+
276+
impl<T> From<TryIntoError<T>> for CustomError {
277+
fn from(value: TryIntoError<T>) -> Self {
278+
Self(value.to_string())
279+
}
280+
}
281+
282+
impl fmt::Display for CustomError {
283+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284+
self.0.fmt(f)
285+
}
286+
}
287+
288+
#[derive(TryInto, Clone, Copy, Debug, Eq, PartialEq)]
289+
#[try_into(ref)]
290+
#[try_into(error(CustomError))]
291+
enum MixedInts {
292+
SmallInt(i32),
293+
NamedBigInt {
294+
int: i64,
295+
},
296+
UnsignedWithIgnoredField(#[try_into(ignore)] bool, i64),
297+
NamedUnsignedWithIgnoredField {
298+
#[try_into(ignore)]
299+
useless: bool,
300+
x: i64,
301+
},
302+
TwoSmallInts(i32, i32),
303+
NamedBigInts {
304+
x: i64,
305+
y: i64,
306+
},
307+
Unsigned(u32),
308+
NamedUnsigned {
309+
x: u32,
310+
},
311+
Unit,
312+
#[try_into(ignore)]
313+
Unit2,
314+
}
315+
316+
#[test]
317+
fn test() {
318+
let i = MixedInts::Unsigned(42);
319+
assert_eq!(
320+
i32::try_from(i).unwrap_err().to_string(),
321+
"Only SmallInt can be converted to i32"
322+
);
323+
assert_eq!(
324+
i64::try_from(i).unwrap_err().to_string(),
325+
"Only NamedBigInt, UnsignedWithIgnoredField, NamedUnsignedWithIgnoredField can be converted to i64"
326+
);
327+
assert_eq!(
328+
<(i32, i32)>::try_from(i).unwrap_err().to_string(),
329+
"Only TwoSmallInts can be converted to (i32, i32)"
330+
);
331+
}
332+
}

0 commit comments

Comments
 (0)