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