sails_macros_core/service/
mod.rs

1//! Supporting functions and structures for the `gservice` macro.
2
3use crate::{
4    sails_paths,
5    shared::{self, FnBuilder},
6};
7use args::ServiceArgs;
8use convert_case::{Case, Casing};
9use proc_macro_error::abort;
10use proc_macro2::{Literal, Span, TokenStream};
11use quote::quote;
12use syn::{Generics, Ident, ItemImpl, Path, Type, TypePath, Visibility, WhereClause};
13
14mod args;
15#[cfg(feature = "ethexe")]
16mod ethexe;
17mod exposure;
18mod meta;
19
20pub fn gservice(args: TokenStream, service_impl: TokenStream) -> TokenStream {
21    let service_impl = parse_gservice_impl(service_impl);
22    ensure_single_gservice_on_impl(&service_impl);
23    generate_gservice(args, service_impl)
24}
25
26#[doc(hidden)]
27pub fn __gservice_internal(args: TokenStream, service_impl: TokenStream) -> TokenStream {
28    let service_impl = parse_gservice_impl(service_impl);
29    generate_gservice(args, service_impl)
30}
31
32fn parse_gservice_impl(service_impl_tokens: TokenStream) -> ItemImpl {
33    syn::parse2(service_impl_tokens).unwrap_or_else(|err| {
34        abort!(
35            err.span(),
36            "`service` attribute can be applied to impls only: {}",
37            err
38        )
39    })
40}
41
42fn ensure_single_gservice_on_impl(service_impl: &ItemImpl) {
43    let attr_gservice = service_impl.attrs.iter().find(|attr| {
44        attr.meta
45            .path()
46            .segments
47            .last()
48            .map(|s| s.ident == "service")
49            .unwrap_or(false)
50    });
51    if attr_gservice.is_some() {
52        abort!(
53            service_impl,
54            "multiple `service` attributes on the same impl are not allowed",
55        )
56    }
57}
58
59struct ServiceBuilder<'a> {
60    service_impl: &'a ItemImpl,
61    sails_path: &'a Path,
62    base_types: &'a [Path],
63    generics: Generics,
64    type_constraints: Option<WhereClause>,
65    type_path: &'a TypePath,
66    events_type: Option<&'a Path>,
67    service_handlers: Vec<FnBuilder<'a>>,
68    exposure_ident: Ident,
69    route_ident: Ident,
70    inner_ident: Ident,
71    input_ident: Ident,
72    meta_module_ident: Ident,
73}
74
75impl<'a> ServiceBuilder<'a> {
76    fn from(
77        service_impl: &'a ItemImpl,
78        sails_path: &'a Path,
79        service_args: &'a ServiceArgs,
80    ) -> Self {
81        let (generics, type_constraints) = shared::impl_constraints(service_impl);
82        let (type_path, _type_args, service_ident) =
83            shared::impl_type_refs(service_impl.self_ty.as_ref());
84        let service_handlers = discover_service_handlers(service_impl, sails_path);
85        let exposure_name = format!(
86            "{}Exposure",
87            service_ident.to_string().to_case(Case::Pascal)
88        );
89        let exposure_ident = Ident::new(&exposure_name, Span::call_site());
90        let route_ident = Ident::new("route", Span::call_site());
91        let inner_ident = Ident::new("inner", Span::call_site());
92        let input_ident = Ident::new("input", Span::call_site());
93        let meta_module_name = format!("{}_meta", service_ident.to_string().to_case(Case::Snake));
94        let meta_module_ident = Ident::new(&meta_module_name, Span::call_site());
95
96        Self {
97            service_impl,
98            sails_path,
99            base_types: service_args.base_types(),
100            generics,
101            type_constraints,
102            type_path,
103            events_type: service_args.events_type(),
104            service_handlers,
105            exposure_ident,
106            route_ident,
107            inner_ident,
108            input_ident,
109            meta_module_ident,
110        }
111    }
112
113    fn type_constraints(&self) -> Option<&WhereClause> {
114        self.type_constraints.as_ref()
115    }
116}
117
118#[cfg(not(feature = "ethexe"))]
119impl ServiceBuilder<'_> {
120    fn service_signature_impl(&self) -> TokenStream {
121        quote!()
122    }
123
124    fn try_handle_solidity_impl(&self) -> TokenStream {
125        quote!()
126    }
127
128    fn exposure_emit_eth_impls(&self) -> Option<TokenStream> {
129        None
130    }
131}
132
133fn generate_gservice(args: TokenStream, service_impl: ItemImpl) -> TokenStream {
134    let service_args = syn::parse2::<ServiceArgs>(args).unwrap_or_else(|err| {
135        abort!(
136            err.span(),
137            "failed to parse `service` attribute arguments: {}",
138            err
139        )
140    });
141    let sails_path = service_args.sails_path();
142
143    let service_builder = ServiceBuilder::from(&service_impl, &sails_path, &service_args);
144
145    if service_builder.service_handlers.is_empty() && service_builder.base_types.is_empty() {
146        abort!(
147            service_builder.service_impl,
148            "`service` attribute requires impl to define at least one public method with `#[export]` macro or extend another service"
149        );
150    }
151
152    let meta_trait_impl = service_builder.meta_trait_impl();
153    let meta_module = service_builder.meta_module();
154
155    let exposure_struct = service_builder.exposure_struct();
156    let exposure_impl = service_builder.exposure_impl();
157    let service_trait_impl = service_builder.service_trait_impl();
158
159    // ethexe
160    let service_signature_impl = service_builder.service_signature_impl();
161
162    quote!(
163        #exposure_struct
164
165        #exposure_impl
166
167        #service_trait_impl
168
169        #meta_trait_impl
170
171        #meta_module
172
173        #service_signature_impl
174    )
175}
176
177fn discover_service_handlers<'a>(
178    service_impl: &'a ItemImpl,
179    sails_path: &'a Path,
180) -> Vec<FnBuilder<'a>> {
181    shared::discover_invocation_targets(
182        service_impl,
183        |fn_item| matches!(fn_item.vis, Visibility::Public(_)) && fn_item.sig.receiver().is_some(),
184        sails_path,
185    )
186    .into_iter()
187    .filter(|fn_builder| fn_builder.export)
188    .collect()
189}
190
191impl FnBuilder<'_> {
192    fn result_type_with_static_lifetime(&self) -> Type {
193        let (result_type, _) = self.result_type_with_value();
194
195        shared::replace_any_lifetime_with_static(result_type.clone())
196    }
197
198    fn handler_meta_variant(&self) -> TokenStream {
199        let handler_route_ident = Ident::new(self.route.as_str(), Span::call_site());
200        let handler_docs_attrs = self
201            .impl_fn
202            .attrs
203            .iter()
204            .filter(|attr| attr.path().is_ident("doc"));
205        let params_struct_ident = &self.params_struct_ident;
206        let result_type = self.result_type_with_static_lifetime();
207
208        let payable_doc = if cfg!(feature = "ethexe") {
209            self.payable.then(|| quote!(#[doc = " #[payable]"]))
210        } else {
211            None
212        };
213
214        let returns_value_doc = if cfg!(feature = "ethexe") {
215            self.result_type_with_value()
216                .1
217                .then(|| quote!(#[doc = " #[returns_value]"]))
218        } else {
219            None
220        };
221        quote!(
222            #( #handler_docs_attrs )*
223            #payable_doc
224            #returns_value_doc
225            #handler_route_ident(#params_struct_ident, #result_type)
226        )
227    }
228
229    fn params_struct(&self, scale_codec_path: &Path, scale_info_path: &Path) -> TokenStream {
230        let params_struct_ident = &self.params_struct_ident;
231        let params_struct_members = self.params().map(|(ident, ty)| quote!(#ident: #ty));
232        let handler_route_bytes = self.encoded_route.as_slice();
233        let is_async = self.is_async();
234
235        quote!(
236            #[derive(Decode, TypeInfo)]
237            #[codec(crate = #scale_codec_path )]
238            #[scale_info(crate = #scale_info_path )]
239            pub struct #params_struct_ident {
240                #(pub(super) #params_struct_members,)*
241            }
242
243            impl InvocationIo for #params_struct_ident {
244                const ROUTE: &'static [u8] = &[ #(#handler_route_bytes),* ];
245                type Params = Self;
246                const ASYNC: bool = #is_async;
247            }
248        )
249    }
250
251    fn try_handle_branch_impl(
252        &self,
253        meta_module_ident: &Ident,
254        input_ident: &Ident,
255    ) -> TokenStream {
256        let handler_func_ident = self.ident;
257
258        let params_struct_ident = &self.params_struct_ident;
259        let handler_func_params = self
260            .params_idents()
261            .iter()
262            .map(|ident| quote!(request.#ident));
263
264        let (result_type, reply_with_value) = self.result_type_with_value();
265        let await_token = self.is_async().then(|| quote!(.await));
266        let unwrap_token = self.unwrap_result.then(|| quote!(.unwrap()));
267
268        let handle_token = if reply_with_value {
269            quote! {
270                let command_reply: CommandReply< #result_type > = self.#handler_func_ident(#(#handler_func_params),*)#await_token #unwrap_token.into();
271                let (result, value) = command_reply.to_tuple();
272            }
273        } else {
274            quote! {
275                let result = self.#handler_func_ident(#(#handler_func_params),*)#await_token #unwrap_token;
276                let value = 0u128;
277            }
278        };
279
280        let result_type = self.result_type_with_static_lifetime();
281
282        let payable_check = {
283            #[cfg(feature = "ethexe")]
284            {
285                self.payable_check()
286            }
287            #[cfg(not(feature = "ethexe"))]
288            {
289                quote!()
290            }
291        };
292
293        quote! {
294            if let Ok(request) = #meta_module_ident::#params_struct_ident::decode_params( #input_ident) {
295                #payable_check
296                #handle_token
297                if !#meta_module_ident::#params_struct_ident::is_empty_tuple::<#result_type>() {
298                    #meta_module_ident::#params_struct_ident::with_optimized_encode(
299                        &result,
300                        self.route().as_ref(),
301                        |encoded_result| result_handler(encoded_result, value),
302                    );
303                }
304                return Some(());
305            }
306        }
307    }
308
309    fn check_asyncness_branch_impl(
310        &self,
311        meta_module_ident: &Ident,
312        input_ident: &Ident,
313    ) -> TokenStream {
314        let params_struct_ident = &self.params_struct_ident;
315
316        quote! {
317            if let Ok(is_async) = #meta_module_ident::#params_struct_ident::check_asyncness( #input_ident) {
318                return Some(is_async);
319            }
320        }
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use quote::quote;
328
329    #[test]
330    fn discover_service_handlers_with_export() {
331        let service_impl = syn::parse2(quote!(
332            impl Service {
333                fn non_public_associated_func_returning_self() -> Self {}
334                fn non_public_associated_func_returning_type() -> Service {}
335                fn non_public_associated_func_returning_smth() -> u32 {}
336                pub fn public_associated_func_returning_self() -> Self {}
337                pub fn public_associated_func_returning_type() -> Service {}
338                pub fn public_associated_func_returning_smth() -> u32 {}
339                fn non_public_method_returning_self(&self) -> Self {}
340                fn non_public_method_returning_type(&self) -> Service {}
341                fn non_public_method_returning_smth(&self) -> u32 {}
342                pub fn public_method_returning_self(&self) -> Self {}
343                pub fn public_method_returning_type(&self) -> Service {}
344                pub fn public_method_returning_smth(&self) -> u32 {}
345                #[export]
346                pub fn export_public_method_returning_self(&self) -> Self {}
347                #[export]
348                pub fn export_public_method_returning_type(&self) -> Service {}
349                #[export]
350                pub fn export_public_method_returning_smth(&self) -> u32 {}
351            }
352        ))
353        .unwrap();
354
355        let sails_path = &sails_paths::sails_path_or_default(None);
356        let discovered_ctors = discover_service_handlers(&service_impl, sails_path)
357            .iter()
358            .map(|fn_builder| fn_builder.ident.to_string())
359            .collect::<Vec<_>>();
360
361        assert_eq!(
362            discovered_ctors,
363            &[
364                "export_public_method_returning_self",
365                "export_public_method_returning_smth",
366                "export_public_method_returning_type"
367            ]
368        );
369    }
370}