web_message_derive/
lib.rs1use 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}