1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{
4 braced,
5 ext::IdentExt,
6 parse::{Parse, ParseStream},
7 parse_macro_input,
8 punctuated::Punctuated,
9 token::{Brace, Const},
10 Attribute, Expr, Ident, Meta, Token, Visibility,
11};
12
13struct FieldLike {
14 pub attrs: Vec<Meta>,
15 pub vis: Visibility,
16 pub constness: Option<Const>,
17 pub ident: Ident,
18 pub colon_token: Option<Token![:]>,
19 pub ty: Ident,
20 pub equals: Token![=],
21 pub defaults: Expr,
22}
23
24impl quote::ToTokens for FieldLike {
25 fn to_tokens(&self, tokens: &mut quote::__private::TokenStream) {
26 let FieldLike {
27 attrs,
28 vis,
29 constness,
30 ident,
31 colon_token,
32 ty,
33 equals,
34 defaults,
35 } = self;
36
37 let ty = format!("{}", ty);
38
39 let (variable_type, numeric) = match ty.as_str() {
40 "f32" => ("Float32", true),
41 "i32" => ("Int32", true),
42 "f64" => ("Float64", true),
43 "i64" => ("Int64", true),
44 "bool" => ("Boolean", false),
45 _ => panic!("unknown variable type"),
46 };
47
48 let ty = format_ident!("{}", variable_type);
49 let default = quote! {
50 #defaults
51 };
52
53 let out = if numeric {
54 let min = attrs
55 .iter()
56 .find(|v| v.path().is_ident("min"))
57 .map(|v| match v {
58 Meta::NameValue(v) => v,
59 _ => panic!("accepts only kv pairs"),
60 })
61 .map_or(quote! {None}, |v| {
62 let lit = &v.lit;
63 quote! { Some(#lit) }
64 });
65
66 let max = attrs
67 .iter()
68 .find(|v| v.path().is_ident("max"))
69 .map(|v| match v {
70 Meta::NameValue(v) => v,
71 _ => panic!("accepts only kv pairs"),
72 })
73 .map_or(quote! {None}, |v| {
74 let lit = &v.lit;
75 quote! { Some(#lit) }
76 });
77
78 quote! {
79 #vis #constness #ident #colon_token tuna::#ty #equals tuna::#ty::new(NAME, stringify!(#ident), #default, #min, #max)
80 }
81 } else {
82 quote! {
83 #vis #constness #ident #colon_token tuna::#ty #equals tuna::#ty::new(NAME, stringify!(#ident), #default)
84 }
85 };
86
87 tokens.extend(out);
88 }
89}
90
91impl Parse for FieldLike {
92 fn parse(input: ParseStream) -> syn::Result<Self> {
93 Ok(FieldLike {
94 attrs: input
95 .call(Attribute::parse_outer)?
96 .iter()
97 .map(|a| a.parse_meta())
98 .collect::<Result<Vec<_>, _>>()?,
99 vis: input.parse()?,
100 constness: input.parse()?,
101 ident: if input.peek(Token![_]) {
102 input.call(Ident::parse_any)
103 } else {
104 input.parse()
105 }?,
106 colon_token: Some(input.parse()?),
107 ty: input.parse()?,
108 equals: input.parse()?,
109 defaults: input.parse()?,
110 })
111 }
112}
113
114#[allow(unused)]
115struct Input {
116 visibility: Visibility,
117 struct_token: Token![mod],
118 name: Ident,
119 brace_token: Brace,
120 fields: Punctuated<FieldLike, Token![;]>,
121}
122
123impl Parse for Input {
124 fn parse(input: ParseStream) -> syn::Result<Self> {
125 let content;
126 let visibility = input.parse()?;
127 let struct_token = input.parse()?;
128 let name = input.parse()?;
129 let brace_token = braced!(content in input);
130 let fields = content.parse_terminated(FieldLike::parse)?;
131 Ok(Input {
132 visibility,
133 struct_token,
134 name,
135 brace_token,
136 fields,
137 })
138 }
139}
140
141#[proc_macro_attribute]
142pub fn tuna(_attr: TokenStream, item: TokenStream) -> TokenStream {
143 let Input { name, fields, .. } = parse_macro_input!(item as Input);
144
145 let fns = fields
146 .iter()
147 .map(|f| {
148 let name = &f.ident;
149 quote! {
150 #name.register();
151 }
152 })
153 .collect::<Vec<_>>();
154 let res = quote!(
155 mod #name {
156 const NAME: &str = stringify!(#name);
157 #fields
158
159 pub fn register() {
160 #(#fns)*
161 }
162 }
163 );
164
165 res.into()
166}