sails_macros_core/service/
mod.rs1use 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 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}