web_message_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{DeriveInput, Fields, 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) => expand_enum(ident, &data_enum.variants),
13		_ => panic!("Message only supports structs and enums (for now)"),
14	};
15
16	output.into()
17}
18
19fn expand_struct(ident: &syn::Ident, fields: &Fields) -> proc_macro2::TokenStream {
20	let field_inits = fields.iter().map(|f| {
21		let name = f.ident.as_ref().unwrap();
22		let name_str = name.to_string();
23
24		quote! {
25			#name: ::web_message::Message::from_message(::web_sys::js_sys::Reflect::get(&obj, &#name_str.into()).map_err(|_| ::web_message::Error::MissingField(#name_str))?)
26				.map_err(|_| ::web_message::Error::InvalidField(#name_str))?
27		}
28	});
29
30	let field_assignments = fields.iter().map(|f| {
31		let name = f.ident.as_ref().unwrap();
32		let name_str = name.to_string();
33
34		quote! {
35			::web_sys::js_sys::Reflect::set(&obj, &#name_str.into(), &self.#name.into()).unwrap();
36		}
37	});
38
39	quote! {
40		impl ::web_message::Message for #ident {
41			fn from_message(message: ::web_sys::js_sys::wasm_bindgen::JsValue) -> Result<Self, ::web_message::Error> {
42				let obj = web_sys::js_sys::Object::try_from(&message).ok_or(::web_message::Error::UnexpectedType)?;
43				Ok(Self {
44					#(#field_inits),*
45				})
46			}
47
48			fn into_message(self, _transferable: &mut ::web_sys::js_sys::Array) -> ::web_sys::js_sys::wasm_bindgen::JsValue {
49				let obj = ::web_sys::js_sys::Object::new();
50				#(#field_assignments)*
51				obj.into()
52			}
53		}
54	}
55}
56
57fn expand_enum(
58	enum_ident: &syn::Ident,
59	variants: &syn::punctuated::Punctuated<syn::Variant, syn::token::Comma>,
60) -> proc_macro2::TokenStream {
61	let from_obj= variants.iter().filter_map(|variant| {
62		let variant_ident = &variant.ident;
63		let variant_str = variant_ident.to_string();
64
65		match &variant.fields {
66			Fields::Named(fields_named) => {
67				let field_assignments = fields_named.named.iter().map(|f| {
68					let name = f.ident.as_ref().unwrap();
69					let name_str = name.to_string();
70
71					quote! {
72						#name: ::web_message::Message::from_message(::web_sys::js_sys::Reflect::get(&val, &#name_str.into()).map_err(|_| ::web_message::Error::MissingField(#name_str))?)
73							.map_err(|_| ::web_message::Error::InvalidField(#name_str))?
74					}
75				});
76
77				Some(quote! {
78					#variant_str if val.is_object() => {
79						Ok(#enum_ident::#variant_ident {
80							#(#field_assignments),*
81						})
82					}
83					#variant_str => Err(::web_message::Error::UnexpectedType),
84				})
85			}
86
87			Fields::Unit => None,
88
89			Fields::Unnamed(fields_unnamed) if fields_unnamed.unnamed.len() == 1 => {
90				Some(quote! {
91					#variant_str => Ok(#enum_ident::#variant_ident(::web_message::Message::from_message(val)?)),
92				})
93			},
94
95			Fields::Unnamed(fields_unnamed) => {
96				let fields_count = fields_unnamed.unnamed.len() as u32;
97				let field_assignments = (0..fields_count).map(|i| {
98					quote! {
99						::web_message::Message::from_message(arr.get(#i))
100							.map_err(|_| ::web_message::Error::InvalidField(stringify!(#i)))?
101					}
102				});
103
104				Some(quote! {
105					#variant_str if val.is_array() => {
106						let arr = ::web_sys::js_sys::Array::from(&val);
107						if arr.length() != #fields_count {
108							return Err(::web_message::Error::UnexpectedLength);
109						}
110
111						Ok(#enum_ident::#variant_ident (
112							#(#field_assignments),*
113						))
114					}
115					#variant_str => Err(::web_message::Error::UnexpectedType),
116				})
117			}
118		}
119	});
120
121	let from_string = variants.iter().filter_map(|variant| {
122		let variant_ident = &variant.ident;
123		let variant_str = variant_ident.to_string();
124
125		if let Fields::Unit = &variant.fields {
126			Some(quote! {
127				#variant_str => Ok(#enum_ident::#variant_ident),
128			})
129		} else {
130			None
131		}
132	});
133
134	let into_matches = variants.iter().map(|variant| {
135		let variant_ident = &variant.ident;
136		let variant_str = variant_ident.to_string();
137
138		match &variant.fields {
139			Fields::Named(fields_named) => {
140				let field_names = fields_named.named.iter().map(|f| f.ident.as_ref().unwrap());
141
142				let set_fields = fields_named.named.iter().map(|f| {
143					let name = f.ident.as_ref().unwrap();
144					let name_str = name.to_string();
145
146					quote! {
147						::web_sys::js_sys::Reflect::set(&inner, &#name_str.into(), &#name.into_message(_transferable)).unwrap();
148					}
149				});
150
151				quote! {
152					#enum_ident::#variant_ident { #(#field_names),* } => {
153						let obj = ::web_sys::js_sys::Object::new();
154						let inner = ::web_sys::js_sys::Object::new();
155						#(#set_fields)*
156						::web_sys::js_sys::Reflect::set(&obj, &#variant_str.into(), &inner.into()).unwrap();
157						obj.into()
158					}
159				}
160			}
161			Fields::Unit => {
162				quote! {
163					#enum_ident::#variant_ident => #variant_str.into()
164				}
165			}
166			Fields::Unnamed(fields_unnamed) if fields_unnamed.unnamed.len() == 1 => {
167				quote! {
168					#enum_ident::#variant_ident(v) => {
169						let obj = ::web_sys::js_sys::Object::new();
170						::web_sys::js_sys::Reflect::set(&obj, &#variant_str.into(), &v.into_message(_transferable)).unwrap();
171						obj.into()
172					}
173				}
174			}
175			Fields::Unnamed(fields_unnamed) => {
176				let fields_count = fields_unnamed.unnamed.len();
177				let field_idents: Vec<_> = (0..fields_count)
178					.map(|i| syn::Ident::new(&format!("field_{}", i), proc_macro2::Span::call_site()))
179					.collect();
180
181				let set_fields = field_idents.iter().map(|f| {
182					quote! {
183						inner.push(&#f.into_message(_transferable));
184					}
185				});
186
187				quote! {
188					#enum_ident::#variant_ident(#(#field_idents),*) => {
189						let obj = ::web_sys::js_sys::Object::new();
190						let inner = ::web_sys::js_sys::Array::new();
191						#(#set_fields)*
192						::web_sys::js_sys::Reflect::set(&obj, &#variant_str.into(), &inner.into()).unwrap();
193						obj.into()
194					}
195				}
196			}
197		}
198	});
199
200	quote! {
201		impl ::web_message::Message for #enum_ident {
202			fn from_message(message: ::web_sys::js_sys::wasm_bindgen::JsValue) -> ::std::result::Result<Self, ::web_message::Error> {
203				if let Some(s) = message.as_string() {
204					match s.as_str() {
205						#(#from_string)*
206						_ => Err(::web_message::Error::UnknownTag),
207					}
208				} else if let Some(obj) = web_sys::js_sys::Object::try_from(&message) {
209					let keys = web_sys::js_sys::Object::keys(&obj);
210					if keys.length() != 1 {
211						return Err(::web_message::Error::UnexpectedType);
212					}
213
214					let tag = keys.get(0);
215					let tag_str = tag.as_string().ok_or(::web_message::Error::UnexpectedType)?;
216
217					let val = ::web_sys::js_sys::Reflect::get(&obj, &tag).unwrap();
218
219					match tag_str.as_str() {
220						#(#from_obj)*
221						_ => Err(::web_message::Error::UnknownTag),
222					}
223				} else {
224					Err(::web_message::Error::UnexpectedType)
225				}
226			}
227
228			fn into_message(self, _transferable: &mut ::web_sys::js_sys::Array) -> ::web_sys::js_sys::wasm_bindgen::JsValue {
229				match self {
230					#(#into_matches),*
231				}
232			}
233		}
234	}
235}