web_message_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{Attribute, DeriveInput, Expr, ExprLit, Fields, Lit, Meta, MetaNameValue, parse_macro_input};
4
5#[proc_macro_derive(Message, attributes(msg))]
6pub fn derive_message(input: TokenStream) -> TokenStream {
7	let input = parse_macro_input!(input as DeriveInput);
8	let ident = &input.ident;
9
10	let output = match &input.data {
11		syn::Data::Struct(data) => expand_struct(ident, &data.fields),
12		syn::Data::Enum(data_enum) => {
13			let tag_field = get_tag_field(&input.attrs).unwrap_or_else(|| {
14				panic!("#[derive(Message)] on enums requires #[msg(tag = \"type\")]");
15			});
16
17			expand_enum(ident, &tag_field, &data_enum.variants)
18		}
19		_ => panic!("Message only supports structs and enums"),
20	};
21
22	output.into()
23}
24
25fn get_tag_field(attrs: &[Attribute]) -> Option<String> {
26	for attr in attrs {
27		if attr.path().is_ident("msg") {
28			if let Ok(Meta::NameValue(MetaNameValue { path, value, .. })) = attr.parse_args() {
29				if path.is_ident("tag") {
30					if let Expr::Lit(ExprLit {
31						lit: Lit::Str(lit_str), ..
32					}) = value
33					{
34						return Some(lit_str.value());
35					}
36				}
37			}
38		}
39	}
40	None
41}
42
43fn expand_struct(ident: &syn::Ident, fields: &Fields) -> proc_macro2::TokenStream {
44	let field_inits = fields.iter().map(|f| {
45		let name = f.ident.as_ref().unwrap();
46		let name_str = name.to_string();
47
48		let is_transferable = f.attrs.iter().any(|attr| {
49			attr.path().is_ident("msg")
50				&& attr
51					.parse_args::<syn::Ident>()
52					.map_or(false, |ident| ident == "transferable")
53		});
54
55		if is_transferable {
56			quote! {
57				#name: ::js_sys::Reflect::get(&obj, &#name_str.into()).map_err(|_| ::web_message::Error::MissingField(#name_str))?
58					.into()
59			}
60		} else {
61			quote! {
62				#name: ::js_sys::Reflect::get(&obj, &#name_str.into()).map_err(|_| ::web_message::Error::MissingField(#name_str))?
63					.try_into()
64					.map_err(|_| ::web_message::Error::InvalidField(#name_str, ::js_sys::Reflect::get(&obj, &#name_str.into()).unwrap()))?
65			}
66		}
67	});
68
69	let field_assignments = fields.iter().map(|f| {
70		let name = f.ident.as_ref().unwrap();
71		let name_str = name.to_string();
72
73		let is_transferable = f.attrs.iter().any(|attr| {
74			attr.path().is_ident("msg")
75				&& attr
76					.parse_args::<syn::Ident>()
77					.map_or(false, |ident| ident == "transferable")
78		});
79
80		if is_transferable {
81			quote! {
82				::js_sys::Reflect::set(&obj, &#name_str.into(), &self.#name.clone().into()).unwrap();
83				transferable.push(&self.#name.into());
84			}
85		} else {
86			quote! {
87				::js_sys::Reflect::set(&obj, &#name_str.into(), &self.#name.into()).unwrap();
88			}
89		}
90	});
91
92	quote! {
93		impl #ident {
94			pub fn from_message(message: ::js_sys::wasm_bindgen::JsValue) -> Result<Self, ::web_message::Error> {
95				let obj = js_sys::Object::try_from(&message).ok_or(::web_message::Error::ExpectedObject(message.clone()))?;
96				Ok(Self {
97					#(#field_inits),*
98				})
99			}
100
101			pub fn into_message(self) -> (::js_sys::Object, ::js_sys::Array) {
102				let obj = ::js_sys::Object::new();
103				let transferable = ::js_sys::Array::new();
104				#(#field_assignments)*
105				(obj, transferable)
106			}
107		}
108	}
109}
110
111fn expand_enum(
112	enum_ident: &syn::Ident,
113	tag_field: &str,
114	variants: &syn::punctuated::Punctuated<syn::Variant, syn::token::Comma>,
115) -> proc_macro2::TokenStream {
116	let from_matches = variants.iter().map(|variant| {
117		let variant_ident = &variant.ident;
118		let variant_str = variant_ident.to_string();
119
120		match &variant.fields {
121			Fields::Named(fields_named) => {
122				let field_assignments = fields_named.named.iter().map(|f| {
123					let name = f.ident.as_ref().unwrap();
124					let name_str = name.to_string();
125
126					let is_transferable = f.attrs.iter().any(|attr| {
127						attr.path().is_ident("post")
128							&& attr
129								.parse_args::<syn::Ident>()
130								.map_or(false, |ident| ident == "transferable")
131					});
132
133					if is_transferable {
134						quote! {
135							#name: ::js_sys::Reflect::get(&obj, &#name_str.into()).map_err(|_| ::web_message::Error::MissingField(#name_str))?
136								.into()
137						}
138					} else {
139						quote! {
140							#name: ::js_sys::Reflect::get(&obj, &#name_str.into()).map_err(|_| ::web_message::Error::MissingField(#name_str))?
141								.try_into()
142								.map_err(|_| ::web_message::Error::InvalidField(#name_str, ::js_sys::Reflect::get(&obj, &#name_str.into()).unwrap()))?
143						}
144					}
145				});
146
147				quote! {
148					#variant_str => {
149						Ok(#enum_ident::#variant_ident {
150							#(#field_assignments),*
151						})
152					}
153				}
154			}
155
156			Fields::Unit => {
157				quote! {
158					#variant_str => Ok(#enum_ident::#variant_ident),
159				}
160			}
161
162			_ => unimplemented!("web-message does not support tuple variants (yet)"),
163		}
164	});
165
166	let into_matches = variants.iter().map(|variant| {
167		let variant_ident = &variant.ident;
168		let variant_str = variant_ident.to_string();
169
170		match &variant.fields {
171			Fields::Named(fields_named) => {
172				let field_names = fields_named.named.iter().map(|f| f.ident.as_ref().unwrap());
173
174				let set_fields = fields_named.named.iter().map(|f| {
175					let name = f.ident.as_ref().unwrap();
176					let name_str = name.to_string();
177
178					let is_transferable = f.attrs.iter().any(|attr| {
179						attr.path().is_ident("post")
180							&& attr
181								.parse_args::<syn::Ident>()
182								.map_or(false, |ident| ident == "transferable")
183					});
184
185					if is_transferable {
186						quote! {
187							::js_sys::Reflect::set(&obj, &#name_str.into(), &#name.clone().into()).unwrap();
188							transferable.push(&#name.into());
189						}
190					} else {
191						quote! {
192							::js_sys::Reflect::set(&obj, &#name_str.into(), &#name.into()).unwrap();
193						}
194					}
195				});
196
197				quote! {
198					#enum_ident::#variant_ident { #(#field_names),* } => {
199						::js_sys::Reflect::set(&obj, &#tag_field.into(), &#variant_str.into()).unwrap();
200						#(#set_fields)*
201					}
202				}
203			}
204			Fields::Unit => {
205				quote! {
206					#enum_ident::#variant_ident => {
207						::js_sys::Reflect::set(&obj, &#tag_field.into(), &#variant_str.into()).unwrap();
208					}
209				}
210			}
211			_ => unimplemented!("web-message does not support tuple variants (yet)"),
212		}
213	});
214
215	quote! {
216		impl #enum_ident {
217			pub fn from_message(message: ::js_sys::wasm_bindgen::JsValue) -> Result<Self, ::web_message::Error> {
218				let obj = js_sys::Object::try_from(&message).ok_or(::web_message::Error::ExpectedObject(message.clone()))?;
219				let tag_val = ::js_sys::Reflect::get(&obj, &#tag_field.into()).map_err(|_| ::web_message::Error::MissingTag(#tag_field))?;
220				let tag_str = tag_val.as_string()
221					.ok_or(::web_message::Error::InvalidTag(#tag_field, tag_val.clone()))?;
222
223				match tag_str.as_str() {
224					#(#from_matches)*
225					_ => Err(::web_message::Error::UnknownTag(#tag_field, tag_val.clone())),
226				}
227			}
228
229			pub fn into_message(self) -> (::js_sys::Object, ::js_sys::Array) {
230				let obj = ::js_sys::Object::new();
231				let transferable = ::js_sys::Array::new();
232
233				match self {
234					#(#into_matches),*
235				}
236
237				(obj, transferable)
238			}
239		}
240	}
241}