Skip to main content

web_rpc_macro/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::TokenStream as TokenStream2;
3use quote::{format_ident, quote, quote_spanned, ToTokens};
4use syn::{
5    braced,
6    ext::IdentExt,
7    parenthesized,
8    parse::{Parse, ParseStream},
9    parse_macro_input, parse_quote,
10    punctuated::Punctuated,
11    spanned::Spanned,
12    Attribute, FnArg, Ident, Lifetime, Pat, PatType, Path, ReturnType, Token, Type, Visibility,
13};
14
15macro_rules! extend_errors {
16    ($errors: ident, $e: expr) => {
17        match $errors {
18            Ok(_) => $errors = Err($e),
19            Err(ref mut errors) => errors.extend($e),
20        }
21    };
22}
23
24// ---------------------------------------------------------------------------
25// Signature types
26// ---------------------------------------------------------------------------
27
28/// If `ty` is `impl Stream<Item = T>`, returns Some(T).
29fn stream_item_type(ty: &Type) -> Option<&Type> {
30    let Type::ImplTrait(impl_trait) = ty else {
31        return None;
32    };
33    for bound in &impl_trait.bounds {
34        let syn::TypeParamBound::Trait(trait_bound) = bound else {
35            continue;
36        };
37        let last_segment = trait_bound.path.segments.last()?;
38        if last_segment.ident != "Stream" {
39            continue;
40        }
41        let syn::PathArguments::AngleBracketed(arguments) = &last_segment.arguments else {
42            continue;
43        };
44        for argument in &arguments.args {
45            if let syn::GenericArgument::AssocType(associated) = argument {
46                if associated.ident == "Item" {
47                    return Some(&associated.ty);
48                }
49            }
50        }
51    }
52    None
53}
54
55/// The type arguments of `ty` if its last path segment is `wrapper<..>`.
56fn type_arguments<'a>(ty: &'a Type, wrapper: &str) -> Option<Vec<&'a Type>> {
57    let Type::Path(type_path) = ty else {
58        return None;
59    };
60    let last_segment = type_path.path.segments.last()?;
61    if last_segment.ident != wrapper {
62        return None;
63    }
64    let syn::PathArguments::AngleBracketed(arguments) = &last_segment.arguments else {
65        return None;
66    };
67    arguments
68        .args
69        .iter()
70        .map(|argument| match argument {
71            syn::GenericArgument::Type(ty) => Some(ty),
72            _ => None,
73        })
74        .collect()
75}
76
77/// If `ty` is `Option<T>`, returns Some(T).
78fn option_inner_type(ty: &Type) -> Option<&Type> {
79    match type_arguments(ty, "Option")?.as_slice() {
80        [inner] => Some(inner),
81        _ => None,
82    }
83}
84
85/// If `ty` is `Result<T, E>`, returns Some((T, E)).
86fn result_inner_types(ty: &Type) -> Option<(&Type, &Type)> {
87    match type_arguments(ty, "Result")?.as_slice() {
88        [ok, err] => Some((ok, err)),
89        _ => None,
90    }
91}
92
93/// If `ty` is `Post<T>` or `Transfer<T>`, returns the inner type and whether it is transferred.
94fn js_inner_type(ty: &Type) -> Option<(&Type, bool)> {
95    for (wrapper, transfer) in [("Post", false), ("Transfer", true)] {
96        if let Some([inner]) = type_arguments(ty, wrapper).as_deref() {
97            return Some((inner, transfer));
98        }
99    }
100    None
101}
102
103/// True if `ty` is `&str` or `&[u8]`, the two reference shapes that keep serde's zero-copy
104/// borrowing path, with an `'a` lifetime injected into the request enum.
105fn is_borrowed_serde_ref(ty: &Type) -> bool {
106    let Type::Reference(reference) = ty else {
107        return false;
108    };
109    match &*reference.elem {
110        Type::Path(path) => path.path.is_ident("str"),
111        Type::Slice(slice) => matches!(&*slice.elem, Type::Path(path) if path.path.is_ident("u8")),
112        _ => false,
113    }
114}
115
116/// True if `attr` is a cfg-style attribute (`#[cfg(...)]` or `#[cfg_attr(...)]`).
117/// These are propagated onto every generated artifact derived from a method so
118/// that rustc strips them in lockstep after macro expansion.
119fn is_cfg_attr(attr: &Attribute) -> bool {
120    attr.path().is_ident("cfg") || attr.path().is_ident("cfg_attr")
121}
122
123/// The `#[cfg(...)]` predicates on an item, which decide whether it survives compilation.
124/// `#[cfg_attr(...)]` rewrites attributes rather than presence and is not included.
125fn cfg_predicates(attrs: &[Attribute]) -> Vec<TokenStream2> {
126    attrs
127        .iter()
128        .filter(|attr| attr.path().is_ident("cfg"))
129        .filter_map(|attr| attr.parse_args::<TokenStream2>().ok())
130        .collect()
131}
132
133// ---------------------------------------------------------------------------
134// Routing
135// ---------------------------------------------------------------------------
136
137/// Recursively emit code that encodes a value of type `ty` into a `WireArg`, pushing Javascript
138/// values onto `post_args` and, for `Transfer`, onto `transfer_args` as a side effect.
139///
140/// The emitted code matches on `&value`, so the caller's binding stays usable, and match
141/// ergonomics binds `__inner` as a reference inside each arm.
142fn emit_encode(
143    ty: &Type,
144    value: TokenStream2,
145    post_args: &TokenStream2,
146    transfer_args: &TokenStream2,
147) -> TokenStream2 {
148    if let Some(inner) = option_inner_type(ty) {
149        let inner_encode = emit_encode(inner, quote!(__inner), post_args, transfer_args);
150        quote_spanned! {ty.span()=>
151            match &#value {
152                ::core::option::Option::Some(__inner) =>
153                    web_rpc::codec::WireArg::Some(::std::boxed::Box::new(#inner_encode)),
154                ::core::option::Option::None =>
155                    web_rpc::codec::WireArg::None,
156            }
157        }
158    } else if let Some((ok, err)) = result_inner_types(ty) {
159        let ok_encode = emit_encode(ok, quote!(__inner), post_args, transfer_args);
160        let err_encode = emit_encode(err, quote!(__inner), post_args, transfer_args);
161        quote_spanned! {ty.span()=>
162            match &#value {
163                ::core::result::Result::Ok(__inner) =>
164                    web_rpc::codec::WireArg::Ok(::std::boxed::Box::new(#ok_encode)),
165                ::core::result::Result::Err(__inner) =>
166                    web_rpc::codec::WireArg::Err(::std::boxed::Box::new(#err_encode)),
167            }
168        }
169    } else if let Some((_, transfer)) = js_inner_type(ty) {
170        let push_transfer = transfer.then(|| {
171            quote! { (#transfer_args).push(web_rpc::wrap::js_value(&#value.0)); }
172        });
173        quote_spanned! {ty.span()=>
174            {
175                (#post_args).push(web_rpc::wrap::js_value(&#value.0));
176                #push_transfer
177                web_rpc::codec::WireArg::Js
178            }
179        }
180    } else {
181        quote_spanned! {ty.span()=>
182            web_rpc::codec::WireArg::Bytes(
183                web_rpc::postcard::to_allocvec(&#value).unwrap()
184            )
185        }
186    }
187}
188
189/// Recursively emit code that decodes a `WireArg` of type `ty` into a Rust value, shifting
190/// Javascript values off `js_values` as needed.
191fn emit_decode(ty: &Type, wire: TokenStream2, js_values: &TokenStream2) -> TokenStream2 {
192    if let Some(inner) = option_inner_type(ty) {
193        let inner_decode = emit_decode(inner, quote!(*__inner), js_values);
194        quote_spanned! {ty.span()=>
195            match #wire {
196                web_rpc::codec::WireArg::Some(__inner) =>
197                    ::core::option::Option::Some(#inner_decode),
198                web_rpc::codec::WireArg::None =>
199                    ::core::option::Option::None,
200                _ => panic!("web_rpc: wire/type mismatch, expected Some or None"),
201            }
202        }
203    } else if let Some((ok, err)) = result_inner_types(ty) {
204        let ok_decode = emit_decode(ok, quote!(*__inner), js_values);
205        let err_decode = emit_decode(err, quote!(*__inner), js_values);
206        quote_spanned! {ty.span()=>
207            match #wire {
208                web_rpc::codec::WireArg::Ok(__inner) =>
209                    ::core::result::Result::Ok(#ok_decode),
210                web_rpc::codec::WireArg::Err(__inner) =>
211                    ::core::result::Result::Err(#err_decode),
212                _ => panic!("web_rpc: wire/type mismatch, expected Ok or Err"),
213            }
214        }
215    } else if let Some((inner, _)) = js_inner_type(ty) {
216        quote_spanned! {ty.span()=>
217            match #wire {
218                web_rpc::codec::WireArg::Js => <#ty>::new(
219                    web_rpc::wasm_bindgen::JsCast::dyn_into::<#inner>((#js_values).shift()).unwrap()
220                ),
221                _ => panic!("web_rpc: wire/type mismatch, expected a Javascript value"),
222            }
223        }
224    } else {
225        quote_spanned! {ty.span()=>
226            match #wire {
227                web_rpc::codec::WireArg::Bytes(__bytes) =>
228                    web_rpc::postcard::from_bytes::<#ty>(&__bytes).unwrap(),
229                _ => panic!("web_rpc: wire/type mismatch, expected postcard bytes"),
230            }
231        }
232    }
233}
234
235/// Emit the `&'static Desc` describing how a value of type `ty` crosses the channel.
236///
237/// The `Schema` and `JsName` bounds are expressed at the signature type's own span, so a
238/// missing derive is reported at the argument or return type rather than inside the expansion.
239fn emit_desc(ty: &Type) -> TokenStream2 {
240    if let Some(inner) = option_inner_type(ty) {
241        let inner_desc = emit_desc(inner);
242        quote_spanned!(ty.span()=> &web_rpc::describe::Desc::Option(#inner_desc))
243    } else if let Some((ok, err)) = result_inner_types(ty) {
244        let ok_desc = emit_desc(ok);
245        let err_desc = emit_desc(err);
246        quote_spanned!(ty.span()=> &web_rpc::describe::Desc::Result(#ok_desc, #err_desc))
247    } else if let Some((inner, transfer)) = js_inner_type(ty) {
248        quote_spanned! {inner.span()=>
249            &web_rpc::describe::Desc::Js {
250                name: <#inner as web_rpc::describe::JsName>::NAME,
251                transfer: #transfer,
252            }
253        }
254    } else if is_borrowed_serde_ref(ty) {
255        // postcard-schema implements `Schema` for `[T]` but not for `str`; the borrowed forms
256        // encode identically to their owned counterparts.
257        let schema_ty: Type = match ty {
258            Type::Reference(reference) => match &*reference.elem {
259                Type::Path(path) if path.path.is_ident("str") => {
260                    parse_quote!(::std::string::String)
261                }
262                other => other.clone(),
263            },
264            other => other.clone(),
265        };
266        quote_spanned! {ty.span()=>
267            &web_rpc::describe::Desc::Inline(
268                <#schema_ty as web_rpc::postcard_schema::Schema>::SCHEMA
269            )
270        }
271    } else {
272        quote_spanned! {ty.span()=>
273            &web_rpc::describe::Desc::Postcard(
274                <#ty as web_rpc::postcard_schema::Schema>::SCHEMA
275            )
276        }
277    }
278}
279
280// ---------------------------------------------------------------------------
281// The parsed trait
282// ---------------------------------------------------------------------------
283
284struct Service {
285    attrs: Vec<Attribute>,
286    vis: Visibility,
287    ident: Ident,
288    methods: Vec<RpcMethod>,
289}
290
291/// What a method sends back.
292enum MethodOutput {
293    Notify,
294    Value(Type),
295    Stream(Type),
296}
297
298struct RpcMethod {
299    is_async: Option<Token![async]>,
300    attrs: Vec<Attribute>,
301    receiver: syn::Receiver,
302    ident: Ident,
303    args: Vec<PatType>,
304    output: MethodOutput,
305}
306
307impl RpcMethod {
308    /// The identifiers of the arguments. Patterns are rejected at parse time, so every
309    /// argument has one.
310    fn argument_idents(&self) -> impl Iterator<Item = &Ident> {
311        self.args.iter().map(|argument| match &*argument.pat {
312            Pat::Ident(pattern) => &pattern.ident,
313            _ => unreachable!("argument patterns are rejected while parsing"),
314        })
315    }
316
317    /// The `#[cfg]` and `#[cfg_attr]` attributes, propagated to everything derived from
318    /// this method.
319    fn cfg_attrs(&self) -> impl Iterator<Item = &Attribute> {
320        self.attrs.iter().filter(|attr| is_cfg_attr(attr))
321    }
322
323    /// The name of this method's variant in the request and response enums.
324    fn variant_ident(&self) -> Ident {
325        Ident::new(
326            &snake_to_camel(&self.ident.unraw().to_string()),
327            self.ident.span(),
328        )
329    }
330
331    /// The name of this method on the Javascript side.
332    fn wire_name(&self) -> String {
333        to_lower_camel(&self.ident.unraw().to_string())
334    }
335
336    /// The return type as written in the generated trait and forwarding impls.
337    fn return_tokens(&self) -> TokenStream2 {
338        match &self.output {
339            MethodOutput::Notify => quote!(),
340            MethodOutput::Value(ty) => quote!(-> #ty),
341            MethodOutput::Stream(item) => {
342                quote!(-> impl web_rpc::futures_core::Stream<Item = #item>)
343            }
344        }
345    }
346}
347
348// ---------------------------------------------------------------------------
349// Code generation
350// ---------------------------------------------------------------------------
351
352struct ServiceGenerator<'a> {
353    trait_ident: &'a Ident,
354    service_ident: Ident,
355    client_ident: Ident,
356    request_ident: Ident,
357    response_ident: Ident,
358    description_ident: Ident,
359    vis: &'a Visibility,
360    attrs: &'a [Attribute],
361    methods: &'a [RpcMethod],
362}
363
364impl ServiceGenerator<'_> {
365    fn enum_request(&self) -> TokenStream2 {
366        let Self {
367            vis,
368            request_ident,
369            methods,
370            ..
371        } = self;
372        let variants = methods.iter().map(|method| {
373            let cfg_attrs = method.cfg_attrs();
374            let variant_ident = method.variant_ident();
375            let fields = method.args.iter().map(|argument| {
376                let pat = &argument.pat;
377                if is_borrowed_serde_ref(&argument.ty) {
378                    // `&str` / `&[u8]` keep the zero-copy serde borrowing path.
379                    let Type::Reference(reference) = &*argument.ty else {
380                        unreachable!("is_borrowed_serde_ref guarantees a reference")
381                    };
382                    let mut reference = reference.clone();
383                    reference.lifetime = Some(Lifetime::new("'a", reference.and_token.span()));
384                    quote_spanned! {argument.ty.span()=> #pat: #reference }
385                } else {
386                    quote_spanned! {argument.ty.span()=> #pat: web_rpc::codec::WireArg }
387                }
388            });
389            quote! {
390                #(#cfg_attrs)*
391                #variant_ident { #( #fields ),* }
392            }
393        });
394        // The hidden variant uses `'a` so that the enum stays well-formed when every borrowing
395        // method is stripped by cfg. It is never constructed; the server's match arm panics on
396        // it.
397        quote! {
398            #[derive(web_rpc::serde::Serialize, web_rpc::serde::Deserialize)]
399            #vis enum #request_ident<'a> {
400                #( #variants, )*
401                #[doc(hidden)]
402                __WebRpcPhantom(::std::marker::PhantomData<&'a ()>),
403            }
404        }
405    }
406
407    fn enum_response(&self) -> TokenStream2 {
408        let Self {
409            vis,
410            response_ident,
411            methods,
412            ..
413        } = self;
414        // Every method gets a variant so that variant indices match the request enum. A
415        // notification's variant is never constructed.
416        let variants = methods.iter().map(|method| {
417            let cfg_attrs = method.cfg_attrs();
418            let variant_ident = method.variant_ident();
419            quote! {
420                #(#cfg_attrs)*
421                #variant_ident ( web_rpc::codec::WireArg )
422            }
423        });
424        quote! {
425            #[derive(web_rpc::serde::Serialize, web_rpc::serde::Deserialize)]
426            #vis enum #response_ident {
427                #( #variants ),*
428            }
429        }
430    }
431
432    /// The compile-time description of the trait, from which `js::endpoint!` renders
433    /// Javascript and Typescript.
434    fn const_description(&self) -> TokenStream2 {
435        let Self {
436            vis,
437            attrs,
438            trait_ident,
439            description_ident,
440            methods,
441            ..
442        } = self;
443
444        let method_const_idents = (0..methods.len())
445            .map(|index| format_ident!("__WEB_RPC_{}_M{}", screaming_snake(trait_ident), index))
446            .collect::<Vec<_>>();
447
448        let method_consts =
449            methods
450                .iter()
451                .zip(&method_const_idents)
452                .map(|(method, const_ident)| {
453                    let args = method.args.iter().zip(method.argument_idents()).map(
454                        |(argument, ident)| {
455                            let name = to_lower_camel(&ident.unraw().to_string());
456                            let desc = emit_desc(&argument.ty);
457                            quote! { web_rpc::describe::Arg { name: #name, desc: #desc } }
458                        },
459                    );
460                    let ret = match &method.output {
461                        MethodOutput::Notify => quote!(web_rpc::describe::Return::Notify),
462                        MethodOutput::Value(ty) => {
463                            let desc = emit_desc(ty);
464                            quote!(web_rpc::describe::Return::Value(#desc))
465                        }
466                        MethodOutput::Stream(item) => {
467                            let desc = emit_desc(item);
468                            quote!(web_rpc::describe::Return::Stream(#desc))
469                        }
470                    };
471                    let wire_name = method.wire_name();
472                    let predicates = cfg_predicates(&method.attrs);
473                    let (enabled, disabled) = if predicates.is_empty() {
474                        (quote!(), quote!(#[cfg(any())]))
475                    } else {
476                        (
477                            quote!(#[cfg(all(#( #predicates ),*))]),
478                            quote!(#[cfg(not(all(#( #predicates ),*)))]),
479                        )
480                    };
481                    quote! {
482                        #enabled
483                        #[doc(hidden)]
484                        const #const_ident: &'static [web_rpc::describe::Method] =
485                            &[web_rpc::describe::Method {
486                                name: #wire_name,
487                                args: &[ #( #args ),* ],
488                                ret: #ret,
489                            }];
490                        #disabled
491                        #[doc(hidden)]
492                        const #const_ident: &'static [web_rpc::describe::Method] = &[];
493                    }
494                });
495
496        let trait_name = trait_ident.to_string();
497        let trait_cfgs = attrs
498            .iter()
499            .filter(|attr| is_cfg_attr(attr))
500            .collect::<Vec<_>>();
501        quote! {
502            #( #method_consts )*
503            #( #trait_cfgs )*
504            #[doc(hidden)]
505            #[allow(non_upper_case_globals)]
506            #vis const #description_ident: &'static web_rpc::describe::Service =
507                &web_rpc::describe::Service {
508                    name: #trait_name,
509                    methods: &[ #( #method_const_idents ),* ],
510                };
511        }
512    }
513
514    fn trait_service(&self) -> TokenStream2 {
515        let Self {
516            attrs,
517            methods,
518            vis,
519            trait_ident,
520            ..
521        } = self;
522
523        let declarations = methods.iter().map(|method| {
524            let RpcMethod {
525                attrs,
526                args,
527                receiver,
528                ident,
529                is_async,
530                ..
531            } = method;
532            let output = method.return_tokens();
533            quote_spanned! {ident.span()=>
534                #( #attrs )*
535                #is_async fn #ident(#receiver, #( #args ),*) #output;
536            }
537        });
538
539        let forwards = methods
540            .iter()
541            .map(|method| {
542                let RpcMethod {
543                    attrs,
544                    args,
545                    receiver,
546                    ident,
547                    is_async,
548                    ..
549                } = method;
550                let output = method.return_tokens();
551                let do_await = is_async.map(|token| quote_spanned!(token.span=> .await));
552                let argument_idents = method.argument_idents();
553                quote_spanned! {ident.span()=>
554                    #( #attrs )*
555                    #is_async fn #ident(#receiver, #( #args ),*) #output {
556                        T::#ident(self, #( #argument_idents ),*)#do_await
557                    }
558                }
559            })
560            .collect::<Vec<_>>();
561
562        quote! {
563            #( #attrs )*
564            #[allow(async_fn_in_trait)]
565            #vis trait #trait_ident {
566                #( #declarations )*
567            }
568
569            impl<T> #trait_ident for ::std::sync::Arc<T> where T: #trait_ident {
570                #( #forwards )*
571            }
572            impl<T> #trait_ident for ::std::boxed::Box<T> where T: #trait_ident {
573                #( #forwards )*
574            }
575            impl<T> #trait_ident for ::std::rc::Rc<T> where T: #trait_ident {
576                #( #forwards )*
577            }
578        }
579    }
580
581    fn struct_client(&self) -> TokenStream2 {
582        let Self {
583            vis,
584            client_ident,
585            request_ident,
586            response_ident,
587            methods,
588            ..
589        } = self;
590
591        let rpc_fns = methods.iter().map(|method| {
592            let RpcMethod {
593                attrs, args, ident, ..
594            } = method;
595            let variant_ident = method.variant_ident();
596
597            // Borrowed `&str`/`&[u8]` pass through inline; everything else becomes a
598            // `WireArg`, pushing onto the post and transfer arrays as it goes.
599            let mut encodings = Vec::new();
600            let mut request_fields = Vec::new();
601            for (argument, argument_ident) in args.iter().zip(method.argument_idents()) {
602                if is_borrowed_serde_ref(&argument.ty) {
603                    request_fields.push(quote! { #argument_ident });
604                } else {
605                    let wire_ident = format_ident!("__wire_{}", argument_ident);
606                    let encode = emit_encode(
607                        &argument.ty,
608                        quote!(#argument_ident),
609                        &quote!(&__post_args),
610                        &quote!(&__transfer_args),
611                    );
612                    encodings.push(quote! { let #wire_ident = #encode; });
613                    request_fields.push(quote! { #argument_ident: #wire_ident });
614                }
615            }
616
617            let send = quote! {
618                let __post_args = web_rpc::js_sys::Array::new();
619                let __transfer_args = web_rpc::js_sys::Array::new();
620                #( #encodings )*
621                let __request = #request_ident::#variant_ident { #( #request_fields ),* };
622                let __sequence = self.state.send(&__request, &__post_args, &__transfer_args);
623            };
624
625            let unpack = |ty: &Type| {
626                let decode = emit_decode(ty, quote!(__wire), &quote!(&__js_values));
627                quote! {
628                    |__response: #response_ident, __js_values: web_rpc::js_sys::Array| {
629                        let #response_ident::#variant_ident(__wire) = __response else {
630                            panic!("web_rpc: received a response for another method")
631                        };
632                        #decode
633                    }
634                }
635            };
636
637            let (return_type, body) = match &method.output {
638                MethodOutput::Notify => (quote!(()), quote! { #send }),
639                MethodOutput::Value(ty) => {
640                    let unpack = unpack(ty);
641                    (
642                        quote!(web_rpc::client::RequestFuture<#ty>),
643                        quote! {
644                            #send
645                            self.state.request(__sequence, #unpack)
646                        },
647                    )
648                }
649                MethodOutput::Stream(item) => {
650                    let unpack = unpack(item);
651                    (
652                        quote!(web_rpc::client::StreamReceiver<#item>),
653                        quote! {
654                            #send
655                            self.state.stream(__sequence, #unpack)
656                        },
657                    )
658                }
659            };
660
661            quote! {
662                #( #attrs )*
663                #vis fn #ident(&self, #( #args ),*) -> #return_type {
664                    #body
665                }
666            }
667        });
668
669        quote! {
670            #[derive(::core::clone::Clone)]
671            #vis struct #client_ident {
672                state: web_rpc::client::State<#response_ident>,
673            }
674            impl ::std::fmt::Debug for #client_ident {
675                fn fmt(&self, formatter: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
676                    formatter.debug_struct(::std::stringify!(#client_ident)).finish()
677                }
678            }
679            impl web_rpc::client::Client for #client_ident {
680                type Response = #response_ident;
681            }
682            impl ::std::convert::From<web_rpc::client::State<#response_ident>> for #client_ident {
683                fn from(state: web_rpc::client::State<#response_ident>) -> Self {
684                    Self { state }
685                }
686            }
687            impl #client_ident {
688                #( #rpc_fns )*
689            }
690        }
691    }
692
693    fn struct_server(&self) -> TokenStream2 {
694        let Self {
695            vis,
696            trait_ident,
697            service_ident,
698            request_ident,
699            response_ident,
700            methods,
701            ..
702        } = self;
703
704        let handlers = methods.iter().map(|method| {
705            let RpcMethod {
706                is_async,
707                ident,
708                args,
709                ..
710            } = method;
711            let cfg_attrs = method.cfg_attrs();
712            let variant_ident = method.variant_ident();
713
714            // Destructure the request variant. Borrowed arguments bind to their own ident;
715            // everything else binds to `__wire_<ident>` and is decoded below.
716            let mut destructure_fields = Vec::new();
717            let mut decodings = Vec::new();
718            for (argument, argument_ident) in args.iter().zip(method.argument_idents()) {
719                if is_borrowed_serde_ref(&argument.ty) {
720                    destructure_fields.push(quote! { #argument_ident });
721                } else {
722                    let wire_ident = format_ident!("__wire_{}", argument_ident);
723                    let decode =
724                        emit_decode(&argument.ty, quote!(#wire_ident), &quote!(&__js_args));
725                    destructure_fields.push(quote! { #argument_ident: #wire_ident });
726                    decodings.push(quote! { let #argument_ident = #decode; });
727                }
728            }
729            let argument_idents = method.argument_idents().collect::<Vec<_>>();
730            let call = quote! { self.implementation.#ident(#( #argument_idents ),*) };
731
732            let encode_outgoing = |ty: &Type, value: TokenStream2| {
733                let encode = emit_encode(
734                    ty,
735                    value,
736                    &quote!(&__post_args),
737                    &quote!(&__transfer_args),
738                );
739                quote! {
740                    let __post_args = web_rpc::js_sys::Array::new();
741                    let __transfer_args = web_rpc::js_sys::Array::new();
742                    let __wire = #encode;
743                    (#response_ident::#variant_ident(__wire), __post_args, __transfer_args)
744                }
745            };
746
747            let body = match (&method.output, is_async) {
748                (MethodOutput::Notify, None) => quote! {
749                    #call;
750                    web_rpc::service::ExecuteResult::Response(None)
751                },
752                (MethodOutput::Notify, Some(_)) => quote! {
753                    #call.await;
754                    web_rpc::service::ExecuteResult::Response(None)
755                },
756                (MethodOutput::Value(ty), None) => {
757                    let outgoing = encode_outgoing(ty, quote!(__response));
758                    quote! {
759                        let __response = #call;
760                        web_rpc::service::ExecuteResult::Response(Some({ #outgoing }))
761                    }
762                }
763                (MethodOutput::Value(ty), Some(_)) => {
764                    let outgoing = encode_outgoing(ty, quote!(__response));
765                    quote! {
766                        let mut __task = ::std::pin::pin!(web_rpc::futures_util::FutureExt::fuse(#call));
767                        web_rpc::service::ExecuteResult::Response(
768                            web_rpc::futures_util::select! {
769                                _ = __abort_rx => None,
770                                __response = __task => Some({ #outgoing }),
771                            }
772                        )
773                    }
774                }
775                (MethodOutput::Stream(item), is_async) => {
776                    let outgoing = encode_outgoing(item, quote!(__item));
777                    let forward = quote! {
778                        let mut __items = ::std::pin::pin!(__items);
779                        let mut __forward = ::std::pin::pin!(web_rpc::futures_util::FutureExt::fuse(async {
780                            while let Some(__item) = web_rpc::futures_util::StreamExt::next(&mut __items).await {
781                                let __outgoing = { #outgoing };
782                                if __stream_tx.unbounded_send((__sequence, Some(__outgoing))).is_err() {
783                                    break;
784                                }
785                            }
786                        }));
787                        web_rpc::futures_util::select! {
788                            _ = __abort_rx => {},
789                            _ = __forward => {},
790                        }
791                        let _ = __stream_tx.unbounded_send((__sequence, None));
792                        web_rpc::service::ExecuteResult::StreamComplete
793                    };
794                    match is_async {
795                        None => quote! {
796                            let __items = #call;
797                            #forward
798                        },
799                        Some(_) => quote! {
800                            let mut __task = ::std::pin::pin!(web_rpc::futures_util::FutureExt::fuse(#call));
801                            let __items = web_rpc::futures_util::select! {
802                                _ = __abort_rx => None,
803                                __items = __task => Some(__items),
804                            };
805                            match __items {
806                                Some(__items) => { #forward }
807                                None => {
808                                    let _ = __stream_tx.unbounded_send((__sequence, None));
809                                    web_rpc::service::ExecuteResult::StreamComplete
810                                }
811                            }
812                        },
813                    }
814                }
815            };
816
817            quote! {
818                #( #cfg_attrs )*
819                #request_ident::#variant_ident { #( #destructure_fields ),* } => {
820                    #( #decodings )*
821                    #body
822                }
823            }
824        });
825
826        quote! {
827            #vis struct #service_ident<T> {
828                implementation: T
829            }
830            impl<T: #trait_ident> web_rpc::service::Service for #service_ident<T> {
831                type Response = #response_ident;
832                #[allow(unused_mut, unused_variables)]
833                async fn execute(
834                    &self,
835                    __sequence: u32,
836                    mut __abort_rx: web_rpc::futures_channel::oneshot::Receiver<()>,
837                    __payload: ::std::vec::Vec<u8>,
838                    __js_args: web_rpc::js_sys::Array,
839                    __stream_tx: web_rpc::futures_channel::mpsc::UnboundedSender<
840                        web_rpc::service::StreamMessage<Self::Response>
841                    >,
842                ) -> (u32, web_rpc::service::ExecuteResult<Self::Response>) {
843                    let __request: #request_ident<'_> =
844                        web_rpc::postcard::from_bytes(&__payload).unwrap();
845                    let __result = match __request {
846                        #( #handlers )*
847                        #request_ident::__WebRpcPhantom(_) => {
848                            unreachable!("web_rpc: __WebRpcPhantom variant received on wire")
849                        }
850                    };
851                    (__sequence, __result)
852                }
853            }
854            impl<T: #trait_ident> ::std::convert::From<T> for #service_ident<T> {
855                fn from(implementation: T) -> Self {
856                    Self { implementation }
857                }
858            }
859        }
860    }
861}
862
863impl ToTokens for ServiceGenerator<'_> {
864    fn to_tokens(&self, output: &mut TokenStream2) {
865        output.extend([
866            self.enum_request(),
867            self.enum_response(),
868            self.const_description(),
869            self.trait_service(),
870            self.struct_client(),
871            self.struct_server(),
872        ])
873    }
874}
875
876// ---------------------------------------------------------------------------
877// Parsing
878// ---------------------------------------------------------------------------
879
880impl Parse for Service {
881    fn parse(input: ParseStream) -> syn::Result<Self> {
882        let attrs = input.call(Attribute::parse_outer)?;
883        let vis = input.parse()?;
884        input.parse::<Token![trait]>()?;
885        let ident: Ident = input.parse()?;
886        let content;
887        braced!(content in input);
888        let mut methods = Vec::new();
889        while !content.is_empty() {
890            methods.push(content.parse()?);
891        }
892        Ok(Self {
893            attrs,
894            vis,
895            ident,
896            methods,
897        })
898    }
899}
900
901impl Parse for RpcMethod {
902    fn parse(input: ParseStream) -> syn::Result<Self> {
903        let mut errors = Ok(());
904        let attrs = input.call(Attribute::parse_outer)?;
905
906        let is_async = input.parse::<Token![async]>().ok();
907        input.parse::<Token![fn]>()?;
908        let ident: Ident = input.parse()?;
909
910        // Reject generic methods up front: the description needs concrete types.
911        if input.peek(Token![<]) {
912            let generics: syn::Generics = input.parse()?;
913            extend_errors!(
914                errors,
915                syn::Error::new_spanned(
916                    generics,
917                    "web_rpc::service trait methods may not have generic parameters; \
918                     concrete types are required so the macro can route and describe each \
919                     argument."
920                )
921            );
922        }
923
924        let content;
925        parenthesized!(content in input);
926        let mut receiver: Option<syn::Receiver> = None;
927        let mut args = Vec::new();
928        for argument in content.parse_terminated(FnArg::parse, Token![,])? {
929            match argument {
930                FnArg::Typed(typed) => match &*typed.pat {
931                    Pat::Ident(_) => args.push(typed),
932                    _ => extend_errors!(
933                        errors,
934                        syn::Error::new(
935                            typed.pat.span(),
936                            "patterns are not allowed in RPC arguments"
937                        )
938                    ),
939                },
940                FnArg::Receiver(ref parsed) => {
941                    if parsed.reference.is_none() || parsed.mutability.is_some() {
942                        extend_errors!(
943                            errors,
944                            syn::Error::new(
945                                argument.span(),
946                                "RPC methods only support `&self` as a receiver"
947                            )
948                        );
949                    }
950                    receiver = Some(parsed.clone());
951                }
952            }
953        }
954        let receiver = receiver.unwrap_or_else(|| {
955            extend_errors!(
956                errors,
957                syn::Error::new(
958                    ident.span(),
959                    "RPC methods must include `&self` as the first parameter"
960                )
961            );
962            parse_quote!(&self)
963        });
964        let output = match input.parse::<ReturnType>()? {
965            ReturnType::Default => MethodOutput::Notify,
966            ReturnType::Type(_, ty) => match stream_item_type(&ty) {
967                Some(item) => MethodOutput::Stream(item.clone()),
968                None => MethodOutput::Value(*ty),
969            },
970        };
971        input.parse::<Token![;]>()?;
972        errors?;
973
974        Ok(Self {
975            is_async,
976            attrs,
977            receiver,
978            ident,
979            args,
980            output,
981        })
982    }
983}
984
985/// This attribute macro should be applied to traits that need to be turned into RPCs. The macro
986/// consumes the trait and outputs four items in its place. For a trait `Calculator` those are
987/// the structs `CalculatorClient` and `CalculatorService`, a new trait by the same name, and a
988/// `CALCULATOR_DESCRIPTION` const describing the trait for
989/// [`web_rpc::js::endpoint!`](../web_rpc/js/macro.endpoint.html). All methods must include
990/// `&self` as their first parameter.
991#[proc_macro_attribute]
992pub fn service(_attr: TokenStream, input: TokenStream) -> TokenStream {
993    let Service {
994        ref attrs,
995        ref vis,
996        ref ident,
997        ref methods,
998    } = parse_macro_input!(input as Service);
999
1000    ServiceGenerator {
1001        trait_ident: ident,
1002        service_ident: format_ident!("{}Service", ident),
1003        client_ident: format_ident!("{}Client", ident),
1004        request_ident: format_ident!("{}Request", ident),
1005        response_ident: format_ident!("{}Response", ident),
1006        description_ident: format_ident!("{}_DESCRIPTION", screaming_snake(ident)),
1007        vis,
1008        attrs,
1009        methods,
1010    }
1011    .into_token_stream()
1012    .into()
1013}
1014
1015// ---------------------------------------------------------------------------
1016// js::endpoint!
1017// ---------------------------------------------------------------------------
1018
1019/// The parsed arguments of `js::endpoint!`.
1020struct EndpointArgs {
1021    service: Option<Path>,
1022    client: Option<Path>,
1023}
1024
1025impl Parse for EndpointArgs {
1026    fn parse(input: ParseStream) -> syn::Result<Self> {
1027        let mut service = None;
1028        let mut client = None;
1029        for entry in Punctuated::<EndpointArg, Token![,]>::parse_terminated(input)? {
1030            let (slot, path, key) = match entry {
1031                EndpointArg::Service(path) => (&mut service, path, "service"),
1032                EndpointArg::Client(path) => (&mut client, path, "client"),
1033            };
1034            if slot.replace(path).is_some() {
1035                return Err(syn::Error::new(
1036                    input.span(),
1037                    format!("`{key}` is given more than once"),
1038                ));
1039            }
1040        }
1041        if service.is_none() && client.is_none() {
1042            return Err(syn::Error::new(
1043                proc_macro2::Span::call_site(),
1044                "a Javascript endpoint needs at least one of `service = ...` (the trait it \
1045                 implements) and `client = ...` (the trait it calls)",
1046            ));
1047        }
1048        Ok(Self { service, client })
1049    }
1050}
1051
1052enum EndpointArg {
1053    Service(Path),
1054    Client(Path),
1055}
1056
1057impl Parse for EndpointArg {
1058    fn parse(input: ParseStream) -> syn::Result<Self> {
1059        let key: Ident = input.parse()?;
1060        input.parse::<Token![=]>()?;
1061        if key == "service" {
1062            Ok(EndpointArg::Service(input.parse()?))
1063        } else if key == "client" {
1064            Ok(EndpointArg::Client(input.parse()?))
1065        } else {
1066            Err(syn::Error::new(
1067                key.span(),
1068                "expected `service` or `client`",
1069            ))
1070        }
1071    }
1072}
1073
1074/// Rewrite `some::path::FooService` (or `FooClient`) into `some::path::FOO_DESCRIPTION`.
1075fn description_path(path: &Path) -> syn::Result<Path> {
1076    let span = path.span();
1077    let mut path = path.clone();
1078    let last = path
1079        .segments
1080        .last_mut()
1081        .ok_or_else(|| syn::Error::new(span, "expected a generated Service or Client"))?;
1082    let name = last.ident.to_string();
1083    let trait_name = name
1084        .strip_suffix("Service")
1085        .or_else(|| name.strip_suffix("Client"))
1086        .filter(|trait_name| !trait_name.is_empty())
1087        .ok_or_else(|| {
1088            syn::Error::new(
1089                last.ident.span(),
1090                "expected a name generated by #[web_rpc::service], which ends in `Service` or \
1091                 `Client`",
1092            )
1093        })?;
1094    last.ident = format_ident!(
1095        "{}_DESCRIPTION",
1096        screaming_snake_str(trait_name),
1097        span = last.ident.span()
1098    );
1099    last.arguments = syn::PathArguments::None;
1100    Ok(path)
1101}
1102
1103/// Render a Javascript endpoint and a `.d.ts` for the other end of a connection into two custom
1104/// sections of the wasm binary. See the [`web_rpc::js`](../web_rpc/js/index.html) module.
1105#[proc_macro]
1106pub fn endpoint(input: TokenStream) -> TokenStream {
1107    let args = parse_macro_input!(input as EndpointArgs);
1108
1109    let class_path = args.client.as_ref().or(args.service.as_ref()).unwrap();
1110    let class_name = class_path.segments.last().unwrap().ident.to_string();
1111    let snake = snake_case_str(&class_name);
1112    let screaming = screaming_snake_str(&class_name);
1113
1114    let description = |path: Option<&Path>| match path.map(description_path) {
1115        Some(Ok(path)) => Ok(quote!(::core::option::Option::Some(#path))),
1116        Some(Err(error)) => Err(error.to_compile_error()),
1117        None => Ok(quote!(::core::option::Option::None)),
1118    };
1119    let service_description = match description(args.service.as_ref()) {
1120        Ok(tokens) => tokens,
1121        Err(error) => return error.into(),
1122    };
1123    let client_description = match description(args.client.as_ref()) {
1124        Ok(tokens) => tokens,
1125        Err(error) => return error.into(),
1126    };
1127
1128    let endpoint_ident = format_ident!("__WEB_RPC_ENDPOINT_{screaming}");
1129    let js_length_ident = format_ident!("__WEB_RPC_ENDPOINT_{screaming}_JS_LENGTH");
1130    let js_ident = format_ident!("__WEB_RPC_ENDPOINT_{screaming}_JS");
1131    let dts_length_ident = format_ident!("__WEB_RPC_ENDPOINT_{screaming}_DTS_LENGTH");
1132    let dts_ident = format_ident!("__WEB_RPC_ENDPOINT_{screaming}_DTS");
1133    let guard_ident = format_ident!("__web_rpc_{snake}");
1134    let js_section = format!("__web_rpc_{snake}_js");
1135    let dts_section = format!("__web_rpc_{snake}_d_ts");
1136
1137    quote! {
1138        #[doc(hidden)]
1139        const #endpoint_ident: web_rpc::js::Endpoint = web_rpc::js::Endpoint {
1140            class: #class_name,
1141            service: #service_description,
1142            client: #client_description,
1143        };
1144        #[doc(hidden)]
1145        const #js_length_ident: usize = web_rpc::js::render_js::<0>(&#endpoint_ident).length;
1146        #[doc(hidden)]
1147        #[allow(long_running_const_eval)]
1148        const #js_ident: [u8; #js_length_ident] =
1149            web_rpc::js::render_js::<#js_length_ident>(&#endpoint_ident).bytes;
1150        #[doc(hidden)]
1151        const #dts_length_ident: usize = web_rpc::js::render_dts::<0>(&#endpoint_ident).length;
1152        #[doc(hidden)]
1153        #[allow(long_running_const_eval)]
1154        const #dts_ident: [u8; #dts_length_ident] =
1155            web_rpc::js::render_dts::<#dts_length_ident>(&#endpoint_ident).bytes;
1156
1157        const _: () = {
1158            #[used]
1159            #[link_section = #js_section]
1160            static JS: [u8; #js_length_ident] = #js_ident;
1161            #[used]
1162            #[link_section = #dts_section]
1163            static D_TS: [u8; #dts_length_ident] = #dts_ident;
1164            // Two endpoints with the same class name in one binary would concatenate into the
1165            // same custom section, so make that a duplicate symbol error instead.
1166            #[no_mangle]
1167            static #guard_ident: u8 = 0;
1168        };
1169    }
1170    .into()
1171}
1172
1173// ---------------------------------------------------------------------------
1174// Name conversions
1175// ---------------------------------------------------------------------------
1176
1177/// `add_numbers` becomes `AddNumbers`: the variant name of a method.
1178fn snake_to_camel(name: &str) -> String {
1179    let mut camel = String::with_capacity(name.len());
1180    let mut capitalize_next = true;
1181    for character in name.chars() {
1182        match character {
1183            '_' => capitalize_next = true,
1184            character if capitalize_next => {
1185                camel.extend(character.to_uppercase());
1186                capitalize_next = false;
1187            }
1188            character => camel.extend(character.to_lowercase()),
1189        }
1190    }
1191    camel
1192}
1193
1194/// `add_numbers` becomes `addNumbers`: the wire name of a method or an argument.
1195fn to_lower_camel(name: &str) -> String {
1196    let camel = snake_to_camel(name);
1197    let mut characters = camel.chars();
1198    match characters.next() {
1199        Some(first) => first.to_lowercase().chain(characters).collect(),
1200        None => camel,
1201    }
1202}
1203
1204/// `FooBar` becomes `FOO_BAR`: the prefix of the description const.
1205fn screaming_snake(ident: &Ident) -> String {
1206    screaming_snake_str(&ident.to_string())
1207}
1208
1209fn screaming_snake_str(name: &str) -> String {
1210    snake_case_str(name).to_uppercase()
1211}
1212
1213/// `FooBar` becomes `foo_bar`: the section name of an endpoint.
1214fn snake_case_str(name: &str) -> String {
1215    let mut snake = String::with_capacity(name.len() + 4);
1216    for (index, character) in name.chars().enumerate() {
1217        if character.is_uppercase() && index > 0 {
1218            snake.push('_');
1219        }
1220        snake.extend(character.to_lowercase());
1221    }
1222    snake
1223}