1#![cfg_attr(docsrs, feature(doc_cfg))]
2use proc_macro::TokenStream;
14use quote::quote;
15use syn::{DeriveInput, LitStr, Type, parse_macro_input};
16
17#[proc_macro_attribute]
37pub fn react_message(attr: TokenStream, item: TokenStream) -> TokenStream {
38 let name_override = match parse_name_only_attr(attr, "react_message") {
39 Ok(name) => name,
40 Err(e) => return e.to_compile_error().into(),
41 };
42
43 let input = parse_macro_input!(item as DeriveInput);
44 let PayloadParts {
45 ident,
46 impl_generics,
47 ty_generics,
48 where_clause,
49 name,
50 } = payload_parts(&input, name_override);
51
52 quote! {
53 #[derive(::serde::Deserialize, ::ts_rs::TS)]
54 #input
55
56 impl #impl_generics ::bevy::ecs::event::Event for #ident #ty_generics #where_clause {
57 type Trigger<'a> = ::bevy::ecs::event::GlobalTrigger;
58 }
59
60 impl #impl_generics ::bevy_react::ReactPayload for #ident #ty_generics #where_clause {
61 const NAME: &'static str = #name;
62 }
63 }
64 .into()
65}
66
67#[proc_macro_attribute]
88pub fn react_request(attr: TokenStream, item: TokenStream) -> TokenStream {
89 let mut name_override: Option<String> = None;
90 let mut response: Option<Type> = None;
91 let arg_parser = syn::meta::parser(|meta| {
92 if try_parse_name_arg(&meta, &mut name_override)? {
93 Ok(())
94 } else if meta.path.is_ident("response") {
95 response = Some(meta.value()?.parse::<Type>()?);
96 Ok(())
97 } else {
98 Err(meta.error(
99 "unsupported `react_request` argument; expected `name = \"...\"` or `response = Type`",
100 ))
101 }
102 });
103 parse_macro_input!(attr with arg_parser);
104
105 let response = match response {
106 Some(ty) => ty,
107 None => {
108 return syn::Error::new(
109 proc_macro2::Span::call_site(),
110 "`react_request` requires a `response = Type` argument",
111 )
112 .to_compile_error()
113 .into();
114 }
115 };
116
117 let input = parse_macro_input!(item as DeriveInput);
118 let PayloadParts {
119 ident,
120 impl_generics,
121 ty_generics,
122 where_clause,
123 name,
124 } = payload_parts(&input, name_override);
125
126 quote! {
127 #[derive(::serde::Deserialize, ::ts_rs::TS)]
128 #input
129
130 impl #impl_generics ::bevy_react::ReactRequest for #ident #ty_generics #where_clause {
131 const NAME: &'static str = #name;
132 type Response = #response;
133 }
134 }
135 .into()
136}
137
138#[proc_macro_attribute]
152pub fn react_event(attr: TokenStream, item: TokenStream) -> TokenStream {
153 let name_override = match parse_name_only_attr(attr, "react_event") {
154 Ok(name) => name,
155 Err(e) => return e.to_compile_error().into(),
156 };
157
158 let input = parse_macro_input!(item as DeriveInput);
159 let PayloadParts {
160 ident,
161 impl_generics,
162 ty_generics,
163 where_clause,
164 name,
165 } = payload_parts(&input, name_override);
166
167 quote! {
168 #[derive(::serde::Serialize, ::ts_rs::TS)]
169 #input
170
171 impl #impl_generics ::bevy_react::ReactEvent for #ident #ty_generics #where_clause {
172 const NAME: &'static str = #name;
173 }
174 }
175 .into()
176}
177
178fn try_parse_name_arg(
181 meta: &syn::meta::ParseNestedMeta,
182 out: &mut Option<String>,
183) -> syn::Result<bool> {
184 if meta.path.is_ident("name") {
185 *out = Some(meta.value()?.parse::<LitStr>()?.value());
186 Ok(true)
187 } else {
188 Ok(false)
189 }
190}
191
192fn parse_name_only_attr(attr: TokenStream, macro_name: &str) -> syn::Result<Option<String>> {
195 let mut name_override: Option<String> = None;
196 let parser = syn::meta::parser(|meta| {
197 if try_parse_name_arg(&meta, &mut name_override)? {
198 Ok(())
199 } else {
200 Err(meta.error(format!(
201 "unsupported `{macro_name}` argument; expected `name = \"...\"`"
202 )))
203 }
204 });
205 syn::parse::Parser::parse(parser, attr)?;
206 Ok(name_override)
207}
208
209struct PayloadParts<'a> {
211 ident: &'a syn::Ident,
212 impl_generics: syn::ImplGenerics<'a>,
213 ty_generics: syn::TypeGenerics<'a>,
214 where_clause: Option<&'a syn::WhereClause>,
215 name: String,
218}
219
220fn payload_parts<'a>(input: &'a DeriveInput, name_override: Option<String>) -> PayloadParts<'a> {
221 let ident = &input.ident;
222 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
223 PayloadParts {
224 ident,
225 impl_generics,
226 ty_generics,
227 where_clause,
228 name: name_override.unwrap_or_else(|| lower_first(&ident.to_string())),
229 }
230}
231
232fn lower_first(s: &str) -> String {
234 let mut chars = s.chars();
235 match chars.next() {
236 Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
237 None => String::new(),
238 }
239}