Skip to content

Commit d77a3cc

Browse files
committed
MVP
0 parents  commit d77a3cc

File tree

7 files changed

+735
-0
lines changed

7 files changed

+735
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target

Cargo.lock

Lines changed: 87 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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "attribute-derive"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[lib]
7+
8+
[dependencies]
9+
syn = "1"
10+
proc-macro2 = "1"
11+
12+
[dependencies.macro]
13+
path = "macro"
14+
15+
[dev-dependencies.syn]
16+
version = "1"
17+
features = ["full"]
18+
19+
[workspace]
20+
members = ["macro"]

macro/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "macro"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[lib]
7+
proc-macro = true
8+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
9+
10+
[dependencies]
11+
proc-macro-error = "1.0.4"
12+
proc-macro2 = "1.0.36"
13+
quote = "1.0"
14+
syn = "1.0"

macro/src/lib.rs

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
use proc_macro2::{Ident, TokenStream};
2+
use proc_macro_error::{abort, abort_call_site, proc_macro_error, ResultExt};
3+
use quote::quote;
4+
use syn::{
5+
parse_macro_input, parse_quote, punctuated::Punctuated, DataStruct, DeriveInput, Field, Fields,
6+
FieldsNamed, Path, Token,
7+
};
8+
9+
// TODO generally should use fully qualified names for trait function calls
10+
11+
#[proc_macro_error]
12+
#[proc_macro_derive(Attribute, attributes(attribute))]
13+
pub fn attribute_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
14+
let syn: Path = parse_quote!(::attribute_derive::__private::syn);
15+
let pm2: Path = parse_quote!(::attribute_derive::__private::proc_macro2);
16+
let none: Path = parse_quote!(::core::option::Option::None);
17+
let some: Path = parse_quote!(::core::option::Option::Some);
18+
let ok: Path = parse_quote!(::core::result::Result::Ok);
19+
let err: Path = parse_quote!(::core::result::Result::Err);
20+
21+
let DeriveInput {
22+
attrs,
23+
ident,
24+
generics,
25+
data,
26+
..
27+
} = parse_macro_input!(input as DeriveInput);
28+
29+
let attribute_ident: String = attrs
30+
.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
43+
}
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+
});
50+
51+
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
52+
53+
let mut options: Punctuated<TokenStream, Token!(,)> = Punctuated::new();
54+
let mut parsing: Punctuated<TokenStream, Token!(,)> = Punctuated::new();
55+
let mut option_assignments: Punctuated<TokenStream, Token!(;)> = Punctuated::new();
56+
let mut assignments: Punctuated<TokenStream, Token!(,)> = Punctuated::new();
57+
let mut possible_variables: Vec<String> = Vec::new();
58+
59+
match data {
60+
syn::Data::Struct(DataStruct {
61+
fields: Fields::Named(FieldsNamed { named, .. }),
62+
..
63+
}) => {
64+
for Field {
65+
attrs,
66+
ident,
67+
ty,
68+
..
69+
} in named.into_iter()
70+
{
71+
let default: bool =
72+
attrs
73+
.into_iter()
74+
.find_map(|attribute| {
75+
if attribute.path.is_ident("attribute") {
76+
Some(
77+
attribute
78+
.parse_args()
79+
.ok()
80+
.and_then(|ident: Ident| {
81+
if ident == "default" {
82+
Some(true)
83+
} else {
84+
None
85+
}
86+
})
87+
.unwrap_or_else(|| {
88+
abort!(
89+
attribute,
90+
"Only `#[attribute(default)]` is currently supported"
91+
)
92+
}),
93+
)
94+
} else {
95+
None
96+
}
97+
})
98+
.unwrap_or_default();
99+
let ident = ident.expect("named struct fields have idents");
100+
let ident_str = ident.to_string();
101+
102+
options.push(quote!(#ident: Option<#ty>));
103+
104+
let error1 = format!("`{ident}` is specified multiple times");
105+
let error2 = format!("`{ident}` was already specified");
106+
option_assignments.push(quote! {
107+
match (&self.#ident, __other.#ident) {
108+
(#none, __value @ #some(_)) => self.#ident = __value,
109+
(#some(__first), #some(__second)) => {
110+
let mut __error =
111+
#syn::Error::new_spanned(__first, #error1);
112+
__error.combine(#syn::Error::new_spanned(
113+
__second,
114+
#error2,
115+
));
116+
return #err(__error);
117+
}
118+
_ => {}
119+
}
120+
});
121+
122+
parsing.push(quote! {
123+
#ident_str => {
124+
__options.#ident = #some(
125+
::attribute_derive::ConvertParsed::convert(__input.parse()?)?
126+
);
127+
}
128+
});
129+
130+
let error = format!("Mandatory `{ident}` was not specified via the attributes.");
131+
assignments.push(if default {
132+
quote! {
133+
#ident: __options.#ident.unwrap_or_default()
134+
}
135+
} else {
136+
quote! {
137+
#ident: __options.#ident.ok_or_else(||
138+
#syn::Error::new(#pm2::Span::call_site(), #error)
139+
)?
140+
}
141+
});
142+
143+
possible_variables.push(format!("`{ident_str}`"));
144+
}
145+
}
146+
_ => abort_call_site!("Only works on structs with named fields"),
147+
};
148+
149+
let error_invalid_name = if possible_variables.len() > 1 {
150+
let last = possible_variables.pop().unwrap();
151+
format!(
152+
"Supported fields are {} and {}",
153+
possible_variables.join(", "),
154+
last
155+
)
156+
} else {
157+
format!("Only `{}` is allowd field", possible_variables[0])
158+
};
159+
160+
quote! {
161+
impl #impl_generics ::attribute_derive::Attribute for #ident #ty_generics #where_clause {
162+
fn from_attributes(__attrs: impl ::core::iter::IntoIterator<Item = #syn::Attribute>) -> #syn::Result<Self>{
163+
#[derive(::core::default::Default)]
164+
struct __Options{
165+
#options
166+
}
167+
impl __Options {
168+
fn extend_with(&mut self, __other:Self) -> #syn::Result<()>{
169+
#option_assignments
170+
#ok(())
171+
}
172+
}
173+
impl #syn::parse::Parse for __Options {
174+
fn parse(__input: #syn::parse::ParseStream<'_>) -> #syn::Result<Self> {
175+
let mut __options = Self::default();
176+
loop {
177+
if __input.is_empty() {
178+
break;
179+
}
180+
181+
let __variable = #syn::Ident::parse(__input)?;
182+
183+
// Parse `=`
184+
__input.step(|__cursor| match __cursor.punct() {
185+
#some((__punct, __rest))
186+
if __punct.as_char() == '=' && __punct.spacing() == #pm2::Spacing::Alone =>
187+
{
188+
#ok(((), __rest))
189+
}
190+
_ => #err(__cursor.error("Expected assignment `=`")),
191+
})?;
192+
193+
match __variable.to_string().as_str() {
194+
#parsing
195+
_ => {
196+
return #err(#syn::Error::new(
197+
__variable.span(),
198+
#error_invalid_name
199+
))
200+
}
201+
}
202+
203+
if __input.is_empty() {
204+
break;
205+
}
206+
207+
// Parse `,`
208+
__input.step(|__cursor| match __cursor.punct() {
209+
#some((__punct, __rest)) if __punct.as_char() == ',' => #ok(((), __rest)),
210+
_ => #err(__cursor.error("Expected assignment `=`")),
211+
})?;
212+
}
213+
Ok(__options)
214+
}
215+
}
216+
let mut __options = __Options::default();
217+
for __attribute in __attrs {
218+
if __attribute.path.is_ident(#attribute_ident) {
219+
__options.extend_with(__attribute.parse_args()?)?;
220+
}
221+
}
222+
#ok(Self {
223+
#assignments
224+
})
225+
}
226+
}
227+
}
228+
.into()
229+
}

0 commit comments

Comments
 (0)