Skip to content

Commit 2d6b65c

Browse files
committed
custom errors
1 parent e12ec62 commit 2d6b65c

File tree

2 files changed

+137
-65
lines changed

2 files changed

+137
-65
lines changed

macro/src/lib.rs

Lines changed: 100 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use proc_macro_error::{abort, abort_call_site, proc_macro_error, ResultExt};
33
use quote::quote;
44
use syn::{
55
parse_macro_input, parse_quote, punctuated::Punctuated, DataStruct, DeriveInput, Field, Fields,
6-
FieldsNamed, Path, Token,
6+
FieldsNamed, Lit, Meta, MetaList, MetaNameValue, NestedMeta, Path, Token,
77
};
88

99
// TODO generally should use fully qualified names for trait function calls
@@ -26,27 +26,40 @@ pub fn attribute_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStre
2626
..
2727
} = parse_macro_input!(input as DeriveInput);
2828

29-
let attribute_ident: String = attrs
29+
let mut attribute_ident = None;
30+
let mut invalid_field = None;
31+
32+
for attribute in attrs
3033
.into_iter()
31-
.find_map(|attribute| {
32-
if attribute.path.is_ident("attribute") {
33-
let path: Path = attribute.parse_args().unwrap_or_abort();
34-
Some(
35-
path.get_ident()
36-
.unwrap_or_else(|| {
37-
abort_call_site!("Only single idents are currently supported")
38-
})
39-
.to_string(),
40-
)
41-
} else {
42-
None
34+
.filter(|attribute| attribute.path.is_ident("attribute"))
35+
{
36+
const VALID_FORMAT: &str = r#"Expected `#[attribute(ident="name_of_your_attribute", invalid_field="error message")]`"#;
37+
let meta: Meta = attribute.parse_meta().unwrap_or_abort();
38+
if let Meta::List(meta) = meta {
39+
for meta in meta.nested {
40+
if let NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. })) = meta {
41+
match (
42+
path.get_ident()
43+
.unwrap_or_else(|| abort_call_site!(VALID_FORMAT))
44+
.to_string()
45+
.as_str(),
46+
lit,
47+
) {
48+
("ident", Lit::Str(lit)) => attribute_ident = Some(lit.value()),
49+
("invalid_field", Lit::Str(lit)) => invalid_field = Some(lit.value()),
50+
_ => abort_call_site!(VALID_FORMAT),
51+
}
52+
} else {
53+
abort_call_site!(VALID_FORMAT);
54+
}
4355
}
44-
})
45-
.unwrap_or_else(|| {
46-
abort_call_site!(
47-
"You need to specify the attribute path via `#[attribute(name_of_your_attribute)]`"
48-
);
49-
});
56+
}
57+
}
58+
let attribute_ident: String = attribute_ident.unwrap_or_else(|| {
59+
abort_call_site!(
60+
r#"You need to specify the attribute path via `#[attribute(ident="name_of_your_attribute")]`"#
61+
);
62+
});
5063

5164
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
5265

@@ -66,34 +79,51 @@ pub fn attribute_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStre
6679
attrs, ident, ty, ..
6780
} in named.into_iter()
6881
{
69-
let default: bool =
70-
attrs
71-
.into_iter()
72-
.find_map(|attribute| {
73-
if attribute.path.is_ident("attribute") {
74-
Some(
75-
attribute
76-
.parse_args()
77-
.ok()
78-
.and_then(|ident: Ident| {
79-
if ident == "default" {
80-
Some(true)
81-
} else {
82-
None
83-
}
84-
})
85-
.unwrap_or_else(|| {
86-
abort!(
87-
attribute,
88-
"Only `#[attribute(default)]` is currently supported"
89-
)
90-
}),
91-
)
82+
let mut default = false;
83+
let mut missing = None;
84+
let mut expected = None;
85+
for attribute in attrs
86+
.into_iter()
87+
.filter(|attribute| attribute.path.is_ident("attribute"))
88+
{
89+
const VALID_FORMAT: &str = r#"Expected `#[attribute(default, missing="error message", expected="error message"])`"#;
90+
let meta: Meta = attribute.parse_meta().unwrap_or_abort();
91+
match meta {
92+
Meta::List(meta) => {
93+
for meta in meta.nested {
94+
if let NestedMeta::Meta(Meta::NameValue(MetaNameValue {
95+
path,
96+
lit,
97+
..
98+
})) = meta
99+
{
100+
match (
101+
path.get_ident()
102+
.unwrap_or_else(|| abort_call_site!(VALID_FORMAT))
103+
.to_string()
104+
.as_str(),
105+
lit,
106+
) {
107+
("missing", Lit::Str(lit)) => missing = Some(lit.value()),
108+
("expected", Lit::Str(lit)) => expected = Some(lit.value()),
109+
_ => abort_call_site!(VALID_FORMAT),
110+
}
111+
} else {
112+
abort_call_site!(VALID_FORMAT);
113+
}
114+
}
115+
}
116+
Meta::Path(path) => {
117+
if path.is_ident("default") {
118+
default = true;
92119
} else {
93-
None
120+
abort_call_site!(VALID_FORMAT);
94121
}
95-
})
96-
.unwrap_or_default();
122+
}
123+
_ => (),
124+
}
125+
}
126+
97127
let ident = ident.expect("named struct fields have idents");
98128
let ident_str = ident.to_string();
99129

@@ -108,7 +138,7 @@ pub fn attribute_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStre
108138
(#none, __value @ #some(_)) => self.#ident = __value,
109139
(#some(__first), #some(__second)) => {
110140
let mut __error = <<#ty as ::attribute_derive::ConvertParsed>::Type as ::attribute_derive::Error>::error(__first, #error1);
111-
__error.combine(<<#ty as ::attribute_derive::ConvertParsed>::Type as ::attribute_derive::Error>::error(
141+
#syn::Error::combine(&mut __error, <<#ty as ::attribute_derive::ConvertParsed>::Type as ::attribute_derive::Error>::error(
112142
&__second,
113143
#error2,
114144
));
@@ -118,17 +148,23 @@ pub fn attribute_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStre
118148
}
119149
});
120150

151+
let error = if let Some(expected) = expected {
152+
quote! {.map_err(|__error| #syn::Error::new(__error.span(), #expected)) }
153+
} else {
154+
quote!()
155+
};
156+
121157
parsing.push(quote! {
122158
#ident_str => {
123159
__options.#ident = #some(
124-
// ::attribute_derive::ConvertParsed::convert(__input.parse()?)?
125-
// TODO FQN
126-
__input.parse()?
160+
#syn::parse::Parse::parse(__input)#error?
127161
);
128162
}
129163
});
130164

131-
let error = format!("Mandatory `{ident}` was not specified via the attributes.");
165+
let error = missing.unwrap_or_else(|| {
166+
format!("Mandatory `{ident}` was not specified via the attributes.")
167+
});
132168
assignments.push(if default {
133169
quote! {
134170
#ident: __options.#ident.map(|t| ::attribute_derive::ConvertParsed::convert(t)).unwrap_or_else(|| #ok(<#ty as core::default::Default>::default()))?
@@ -149,18 +185,21 @@ pub fn attribute_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStre
149185
_ => abort_call_site!("Only works on structs with named fields"),
150186
};
151187

152-
let error_invalid_name = if possible_variables.len() > 1 {
153-
let last = possible_variables.pop().unwrap();
154-
format!(
155-
"Supported fields are {} and {}",
156-
possible_variables.join(", "),
157-
last
158-
)
159-
} else {
160-
format!("Expected supported field {}", possible_variables[0])
161-
};
188+
let error_invalid_name = invalid_field.unwrap_or_else(|| {
189+
if possible_variables.len() > 1 {
190+
let last = possible_variables.pop().unwrap();
191+
format!(
192+
"Supported fields are {} and {}",
193+
possible_variables.join(", "),
194+
last
195+
)
196+
} else {
197+
format!("Expected supported field {}", possible_variables[0])
198+
}
199+
});
162200

163201
quote! {
202+
#[allow(unreachable_code)]
164203
impl #impl_generics ::attribute_derive::Attribute for #ident #ty_generics #where_clause {
165204
fn from_attributes(__attrs: impl ::core::iter::IntoIterator<Item = #syn::Attribute>) -> #syn::Result<Self>{
166205
struct __Options{

tests/derive.rs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use syn::parse_quote;
55
fn test() {
66
use syn::{Expr, LitStr, Type};
77
#[derive(Attribute)]
8-
#[attribute(test)]
8+
#[attribute(ident = "test")]
99
struct Test {
1010
// a: u8,
1111
b: LitStr,
@@ -33,7 +33,7 @@ fn test() {
3333
#[test]
3434
fn error() {
3535
#[derive(Attribute, Debug)]
36-
#[attribute(test)]
36+
#[attribute(ident = "test")]
3737
struct Test {
3838
#[allow(dead_code)]
3939
s: String,
@@ -71,25 +71,42 @@ fn error() {
7171
#[test]
7272
fn error2() {
7373
#[derive(Attribute, Debug)]
74-
#[attribute(test)]
74+
#[attribute(ident = "test")]
7575
#[allow(dead_code)]
7676
struct Test {
77+
#[attribute(expected = "no")]
7778
a: f32,
7879
b: u8,
80+
#[attribute(missing = "yes")]
81+
c: String,
7982
}
8083

84+
assert_eq!(
85+
Test::from_attributes([parse_quote!(#[test(d="")])])
86+
.unwrap_err()
87+
.to_string(),
88+
"Supported fields are `a`, `b` and `c`"
89+
);
90+
8191
assert_eq!(
8292
Test::from_attributes([parse_quote!(#[test()])])
8393
.unwrap_err()
8494
.to_string(),
8595
"Mandatory `a` was not specified via the attributes."
8696
);
8797

98+
assert_eq!(
99+
Test::from_attributes([parse_quote!(#[test(a=1., b=1)])])
100+
.unwrap_err()
101+
.to_string(),
102+
"yes"
103+
);
104+
88105
assert_eq!(
89106
Test::from_attributes([parse_quote!(#[test(a="")])])
90107
.unwrap_err()
91108
.to_string(),
92-
"expected floating point literal"
109+
"no"
93110
);
94111

95112
assert_eq!(
@@ -114,3 +131,19 @@ fn error2() {
114131
"number too large to fit in target type"
115132
);
116133
}
134+
135+
#[test]
136+
fn error_specified() {
137+
#[derive(Attribute, Debug)]
138+
#[attribute(ident = "test")]
139+
#[attribute(invalid_field = "error message")]
140+
#[allow(dead_code)]
141+
struct Test {}
142+
143+
assert_eq!(
144+
Test::from_attributes([parse_quote!(#[test(c="")])])
145+
.unwrap_err()
146+
.to_string(),
147+
"error message"
148+
);
149+
}

0 commit comments

Comments
 (0)