sails_macros_core/program/
mod.rs

1use crate::{
2    export, sails_paths,
3    shared::{self, FnBuilder, InvocationExport},
4};
5use args::ProgramArgs;
6use proc_macro_error::abort;
7use proc_macro2::{Span, TokenStream as TokenStream2};
8use quote::quote;
9use std::{
10    collections::BTreeMap,
11    env,
12    ops::{Deref, DerefMut},
13};
14use syn::{
15    Generics, Ident, ImplItem, ImplItemFn, ItemImpl, Path, PathArguments, Receiver, ReturnType,
16    Type, TypePath, Visibility, WhereClause, parse_quote, spanned::Spanned,
17};
18
19mod args;
20#[cfg(feature = "ethexe")]
21mod ethexe;
22
23/// Static Spans of Program `impl` block
24static mut PROGRAM_SPANS: BTreeMap<String, Span> = BTreeMap::new();
25
26pub fn gprogram(args: TokenStream2, program_impl_tokens: TokenStream2) -> TokenStream2 {
27    let program_impl = parse_gprogram_impl(program_impl_tokens);
28    ensure_single_gprogram(&program_impl);
29    let args = parse_args(args);
30    gen_gprogram_impl(program_impl, args)
31}
32
33#[doc(hidden)]
34pub fn __gprogram_internal(args: TokenStream2, program_impl_tokens: TokenStream2) -> TokenStream2 {
35    let program_impl = parse_gprogram_impl(program_impl_tokens);
36    let args = parse_args(args);
37    gen_gprogram_impl(program_impl, args)
38}
39
40fn parse_args(args: TokenStream2) -> ProgramArgs {
41    syn::parse2(args).unwrap_or_else(|err| {
42        abort!(
43            err.span(),
44            "failed to parse `program` attribute arguments: {}",
45            err
46        )
47    })
48}
49
50fn parse_gprogram_impl(program_impl_tokens: TokenStream2) -> ItemImpl {
51    syn::parse2(program_impl_tokens).unwrap_or_else(|err| {
52        abort!(
53            err.span(),
54            "`program` attribute can be applied to impls only: {}",
55            err
56        )
57    })
58}
59
60#[allow(static_mut_refs)]
61fn ensure_single_gprogram(program_impl: &ItemImpl) {
62    let crate_name = env::var("CARGO_CRATE_NAME").unwrap_or("crate".to_string());
63    if unsafe { PROGRAM_SPANS.get(&crate_name) }.is_some() {
64        abort!(
65            program_impl,
66            "multiple `program` attributes are not allowed"
67        )
68    }
69    unsafe { PROGRAM_SPANS.insert(crate_name, program_impl.span()) };
70}
71
72struct ProgramBuilder {
73    program_impl: ItemImpl,
74    program_args: ProgramArgs,
75    type_constraints: Option<WhereClause>,
76}
77
78impl ProgramBuilder {
79    fn new(program_impl: ItemImpl, program_args: ProgramArgs) -> Self {
80        let mut program_impl = program_impl;
81        let type_constraints = program_impl.generics.where_clause.take();
82        ensure_default_program_ctor(&mut program_impl);
83
84        Self {
85            program_impl,
86            program_args,
87            type_constraints,
88        }
89    }
90
91    fn sails_path(&self) -> &Path {
92        self.program_args.sails_path()
93    }
94
95    fn impl_type(&self) -> (&TypePath, &PathArguments, &Ident) {
96        shared::impl_type_refs(self.program_impl.self_ty.as_ref())
97    }
98
99    fn impl_constraints(&self) -> (&Generics, Option<&WhereClause>) {
100        (&self.program_impl.generics, self.type_constraints.as_ref())
101    }
102
103    fn program_ctors(&self) -> Vec<FnBuilder<'_>> {
104        discover_program_ctors(&self.program_impl, self.sails_path())
105    }
106
107    fn handle_reply_fn(&mut self) -> Option<&mut ImplItemFn> {
108        let mut fn_iter = self.program_impl.items.iter_mut().filter_map(|item| {
109            if let ImplItem::Fn(fn_item) = item
110                && has_handle_reply_attr(fn_item) {
111                    fn_item
112                        .attrs
113                        .retain(|attr| !attr.path().is_ident("handle_reply"));
114                    if handle_reply_predicate(fn_item) {
115                        return Some(fn_item);
116                    } else {
117                        abort!(
118                            fn_item,
119                            "`handle_reply` function must have a single `&self` argument and no return type"
120                        );
121                    }
122                }
123            None
124        });
125        let handle_reply_fn = fn_iter.next();
126        if let Some(duplicate) = fn_iter.next() {
127            abort!(duplicate, "only one `handle_reply` function is allowed");
128        }
129        handle_reply_fn
130    }
131
132    #[cfg(feature = "ethexe")]
133    fn service_ctors(&self) -> Vec<FnBuilder<'_>> {
134        shared::discover_invocation_targets(self, service_ctor_predicate, self.sails_path())
135    }
136}
137
138impl ProgramBuilder {
139    fn wire_up_service_exposure(
140        &mut self,
141        program_ident: &Ident,
142    ) -> (TokenStream2, TokenStream2, TokenStream2, TokenStream2) {
143        let mut services_route = Vec::new();
144        let mut services_meta = Vec::new();
145        let mut meta_asyncness = Vec::new();
146        let mut invocation_dispatches = Vec::new();
147        let mut routes = BTreeMap::new();
148        // only used for ethexe
149        #[allow(unused_mut)]
150        let mut solidity_dispatchers: Vec<TokenStream2> = Vec::new();
151
152        let has_async_ctor = self
153            .program_ctors()
154            .iter()
155            .any(|fn_builder| fn_builder.is_async());
156
157        if has_async_ctor {
158            meta_asyncness.push(quote!(true));
159        }
160
161        let item_impl = self
162            .program_impl
163            .items
164            .iter()
165            .enumerate()
166            .filter_map(|(idx, impl_item)| {
167                if let ImplItem::Fn(fn_item) = impl_item
168                    && service_ctor_predicate(fn_item)
169                {
170                    let invocation_export = shared::invocation_export_or_default(fn_item);
171
172                    #[cfg(feature = "ethexe")]
173                    {
174                        use convert_case::{Case, Casing};
175                        let camel_case_route = invocation_export.route.to_case(Case::Camel);
176                        shared::validation::validate_identifier(
177                            &camel_case_route,
178                            fn_item.sig.ident.span(),
179                            "Exposed Service",
180                        );
181                    }
182
183                    if let Some(duplicate) = routes.insert(
184                        invocation_export.route.clone(),
185                        fn_item.sig.ident.to_string(),
186                    ) {
187                        abort!(
188                            invocation_export.span,
189                            "`export` attribute conflicts with one already assigned to '{}'",
190                            duplicate
191                        );
192                    }
193                    return Some((idx, fn_item, invocation_export));
194                }
195                None
196            })
197            .map(|(idx, fn_item, invocation_export)| {
198                let InvocationExport {
199                    route,
200                    unwrap_result,
201                    #[cfg(feature = "ethexe")]
202                    payable,
203                    ..
204                } = invocation_export;
205
206                let fn_builder =
207                    FnBuilder::new(route, true, fn_item, unwrap_result, self.sails_path());
208
209                #[cfg(feature = "ethexe")]
210                let fn_builder = fn_builder.payable(payable);
211
212                let original_service_ctor_fn = fn_builder.original_service_ctor_fn();
213                let wrapping_service_ctor_fn =
214                    fn_builder.wrapping_service_ctor_fn(&original_service_ctor_fn.sig.ident);
215
216                services_route.push(fn_builder.service_const_route());
217                services_meta.push(fn_builder.service_meta());
218
219                if !has_async_ctor {
220                    // If there are no async constructors, we can't push the asyncness as false,
221                    // as there could be async handlers in services.
222                    meta_asyncness.push(fn_builder.service_meta_asyncness());
223                }
224                invocation_dispatches.push(fn_builder.service_invocation());
225                #[cfg(feature = "ethexe")]
226                solidity_dispatchers.push(fn_builder.sol_service_invocation());
227
228                (idx, original_service_ctor_fn, wrapping_service_ctor_fn)
229            })
230            .collect::<Vec<_>>();
231
232        if meta_asyncness.is_empty() {
233            // In case non of constructors is async and there are no services exposed.
234            meta_asyncness.push(quote!(false));
235        }
236
237        // replace service ctor fn impls
238        for (idx, original_service_ctor_fn, wrapping_service_ctor_fn, ..) in item_impl {
239            self.program_impl.items[idx] = ImplItem::Fn(original_service_ctor_fn);
240            self.program_impl
241                .items
242                .push(ImplItem::Fn(wrapping_service_ctor_fn));
243        }
244
245        let handle_reply_fn = self.handle_reply_fn().map(|item_fn| {
246            let handle_reply_fn_ident = &item_fn.sig.ident;
247            quote! {
248                let program_ref = unsafe { #program_ident.as_mut() }.expect("Program not initialized");
249                program_ref.#handle_reply_fn_ident();
250            }
251        })
252        .unwrap_or_default();
253
254        let handle_signal_fn = self
255            .program_args
256            .handle_signal()
257            .map(|handle_signal_path| quote!( #handle_signal_path ();))
258            .unwrap_or_default();
259
260        let sails_path = self.sails_path();
261        let (program_type_path, _program_type_args, _) = self.impl_type();
262        let (generics, program_type_constraints) = self.impl_constraints();
263
264        let program_meta_impl = quote! {
265            #(#services_route)*
266
267            impl #generics #sails_path::meta::ProgramMeta for #program_type_path #program_type_constraints {
268                type ConstructorsMeta = meta_in_program::ConstructorsMeta;
269
270                const SERVICES: &'static [(&'static str, #sails_path::meta::AnyServiceMetaFn)] = &[
271                    #(#services_meta),*
272                ];
273                const ASYNC: bool = #( #meta_asyncness )||*;
274            }
275        };
276
277        invocation_dispatches.push(quote! {
278            { gstd::unknown_input_panic("Unexpected service", &input) }
279        });
280
281        let solidity_main = self.sol_main(solidity_dispatchers.as_slice());
282
283        let payable = self.program_args.payable().then(|| {
284            quote! {
285                if gstd::msg::value() > 0 && gstd::msg::size() == 0 {
286                    return;
287                }
288            }
289        });
290
291        let main_fn = quote!(
292            #[unsafe(no_mangle)]
293            extern "C" fn handle() {
294                #payable
295
296                let mut input = gstd::msg::load_bytes().expect("Failed to read input");
297                let program_ref = unsafe { #program_ident.as_mut() }.expect("Program not initialized");
298
299                #solidity_main
300
301                #(#invocation_dispatches)else*;
302            }
303
304        );
305
306        let handle_reply_fn = quote! {
307            #[unsafe(no_mangle)]
308            extern "C" fn handle_reply() {
309                use #sails_path::meta::ProgramMeta;
310
311                if #program_type_path::ASYNC {
312                    gstd::handle_reply_with_hook();
313                }
314
315                #handle_reply_fn
316            }
317        };
318
319        #[cfg(not(feature = "ethexe"))]
320        let handle_signal_fn = quote! {
321            #[unsafe(no_mangle)]
322            extern "C" fn handle_signal() {
323                use #sails_path::meta::ProgramMeta;
324
325                if #program_type_path::ASYNC {
326                    gstd::handle_signal();
327                }
328
329                #handle_signal_fn
330            }
331        };
332
333        (
334            program_meta_impl,
335            main_fn,
336            handle_reply_fn,
337            handle_signal_fn,
338        )
339    }
340
341    fn generate_init(&self, program_ident: &Ident) -> (TokenStream2, TokenStream2) {
342        let sails_path = self.sails_path();
343        let scale_codec_path = sails_paths::scale_codec_path(sails_path);
344        let scale_info_path = sails_paths::scale_info_path(sails_path);
345
346        let (program_type_path, ..) = self.impl_type();
347        let input_ident = Ident::new("input", Span::call_site());
348
349        let program_ctors = self.program_ctors();
350
351        let mut ctor_dispatches = Vec::with_capacity(program_ctors.len() + 1);
352        let mut ctor_params_structs = Vec::with_capacity(program_ctors.len());
353        let mut ctor_meta_variants = Vec::with_capacity(program_ctors.len());
354
355        for fn_builder in program_ctors {
356            ctor_dispatches.push(fn_builder.ctor_branch_impl(
357                program_type_path,
358                &input_ident,
359                program_ident,
360            ));
361            ctor_params_structs
362                .push(fn_builder.ctor_params_struct(&scale_codec_path, &scale_info_path));
363            ctor_meta_variants.push(fn_builder.ctor_meta_variant());
364        }
365
366        ctor_dispatches.push(quote! {
367            { gstd::unknown_input_panic("Unexpected ctor", input) }
368        });
369
370        let solidity_init = self.sol_init(&input_ident);
371
372        let init_fn = quote! {
373            #[unsafe(no_mangle)]
374            extern "C" fn init() {
375                use gstd::InvocationIo;
376
377                let mut #input_ident: &[u8] = &gstd::msg::load_bytes().expect("Failed to read input");
378
379                #solidity_init
380
381                #(#ctor_dispatches)else*;
382            }
383        };
384
385        let meta_in_program = quote! {
386            mod meta_in_program {
387                use super::*;
388                use #sails_path::gstd::InvocationIo;
389
390                #( #ctor_params_structs )*
391
392                #[derive(#sails_path ::TypeInfo)]
393                #[scale_info(crate = #scale_info_path)]
394                pub enum ConstructorsMeta {
395                    #( #ctor_meta_variants ),*
396                }
397            }
398        };
399        (meta_in_program, init_fn)
400    }
401}
402
403// Empty ProgramBuilder Implementations without `ethexe` feature
404#[cfg(not(feature = "ethexe"))]
405impl ProgramBuilder {
406    fn program_signature_impl(&self) -> TokenStream2 {
407        quote!()
408    }
409
410    fn match_ctor_impl(&self, _program_ident: &Ident) -> TokenStream2 {
411        quote!()
412    }
413
414    fn program_const(&self) -> TokenStream2 {
415        quote!()
416    }
417
418    fn sol_init(&self, _input_ident: &Ident) -> TokenStream2 {
419        quote!()
420    }
421
422    fn sol_main(&self, _solidity_dispatchers: &[TokenStream2]) -> TokenStream2 {
423        quote!()
424    }
425}
426
427impl Deref for ProgramBuilder {
428    type Target = ItemImpl;
429
430    fn deref(&self) -> &Self::Target {
431        &self.program_impl
432    }
433}
434
435impl DerefMut for ProgramBuilder {
436    fn deref_mut(&mut self) -> &mut Self::Target {
437        &mut self.program_impl
438    }
439}
440
441fn gen_gprogram_impl(program_impl: ItemImpl, program_args: ProgramArgs) -> TokenStream2 {
442    let mut program_builder = ProgramBuilder::new(program_impl, program_args);
443
444    let sails_path = program_builder.sails_path().clone();
445
446    let program_ident = Ident::new("PROGRAM", Span::call_site());
447
448    // Call this before `wire_up_service_exposure`
449    let program_signature_impl = program_builder.program_signature_impl();
450    let match_ctor_impl = program_builder.match_ctor_impl(&program_ident);
451    let program_const = program_builder.program_const();
452
453    let (program_meta_impl, main_fn, handle_reply_fn, handle_signal_fn) =
454        program_builder.wire_up_service_exposure(&program_ident);
455    let (meta_in_program, init_fn) = program_builder.generate_init(&program_ident);
456
457    let (program_type_path, ..) = program_builder.impl_type();
458
459    let program_impl = program_builder.deref();
460
461    quote!(
462        #program_impl
463
464        #program_meta_impl
465
466        #meta_in_program
467
468        #program_signature_impl
469
470        #program_const
471
472        #[cfg(target_arch = "wasm32")]
473        pub mod wasm {
474            use super::*;
475            use #sails_path::{gstd, hex, prelude::*};
476
477            static mut #program_ident: Option<#program_type_path> = None;
478
479            #init_fn
480
481            #match_ctor_impl
482
483            #main_fn
484
485            #handle_reply_fn
486
487            #handle_signal_fn
488        }
489    )
490}
491
492fn ensure_default_program_ctor(program_impl: &mut ItemImpl) {
493    let sails_path = &sails_paths::sails_path_or_default(None);
494    if discover_program_ctors(program_impl, sails_path).is_empty() {
495        program_impl.items.push(ImplItem::Fn(parse_quote!(
496            pub fn create() -> Self {
497                Default::default()
498            }
499        )));
500    }
501}
502
503fn discover_program_ctors<'a>(
504    program_impl: &'a ItemImpl,
505    sails_path: &'a Path,
506) -> Vec<FnBuilder<'a>> {
507    let self_type_path: TypePath = parse_quote!(Self);
508    let (program_type_path, _, _) = shared::impl_type_refs(program_impl.self_ty.as_ref());
509    let ctors = shared::discover_invocation_targets(
510        program_impl,
511        |fn_item| program_ctor_predicate(fn_item, &self_type_path, program_type_path),
512        sails_path,
513    );
514
515    #[cfg(feature = "ethexe")]
516    {
517        for ctor in &ctors {
518            shared::validation::validate_identifier(
519                &ctor.route_camel_case(),
520                ctor.ident.span(),
521                "Program constructor",
522            );
523        }
524    }
525
526    ctors
527}
528
529fn program_ctor_predicate(
530    fn_item: &ImplItemFn,
531    self_type_path: &TypePath,
532    program_type_path: &TypePath,
533) -> bool {
534    if matches!(fn_item.vis, Visibility::Public(_))
535        && fn_item.sig.receiver().is_none()
536        && let ReturnType::Type(_, output_type) = &fn_item.sig.output
537        && let Type::Path(output_type_path) = output_type.as_ref()
538    {
539        if output_type_path == self_type_path || output_type_path == program_type_path {
540            return true;
541        }
542        if let Some(Type::Path(output_type_path)) = shared::extract_result_type(output_type_path)
543            && (output_type_path == self_type_path || output_type_path == program_type_path)
544        {
545            return true;
546        }
547    }
548    false
549}
550
551fn service_ctor_predicate(fn_item: &ImplItemFn) -> bool {
552    matches!(fn_item.vis, Visibility::Public(_))
553        && matches!(
554            fn_item.sig.receiver(),
555            Some(Receiver {
556                reference: Some(_),
557                ..
558            })
559        )
560        && fn_item.sig.inputs.len() == 1
561        && !matches!(fn_item.sig.output, ReturnType::Default)
562}
563
564fn has_handle_reply_attr(fn_item: &ImplItemFn) -> bool {
565    fn_item
566        .attrs
567        .iter()
568        .any(|attr| attr.path().is_ident("handle_reply"))
569}
570
571fn handle_reply_predicate(fn_item: &ImplItemFn) -> bool {
572    matches!(fn_item.vis, Visibility::Inherited)
573        && matches!(
574            fn_item.sig.receiver(),
575            Some(Receiver {
576                mutability: None,
577                reference: Some(_),
578                ..
579            })
580        )
581        && fn_item.sig.inputs.len() == 1
582        && matches!(fn_item.sig.output, ReturnType::Default)
583}
584
585impl FnBuilder<'_> {
586    fn route_ident(&self) -> Ident {
587        Ident::new(
588            &format!("__ROUTE_{}", self.route.to_ascii_uppercase()),
589            Span::call_site(),
590        )
591    }
592
593    fn service_meta(&self) -> TokenStream2 {
594        let sails_path = self.sails_path;
595        let route = &self.route;
596        let service_type = &self.result_type;
597        quote!(
598            ( #route , #sails_path::meta::AnyServiceMeta::new::< #service_type >)
599        )
600    }
601
602    fn service_meta_asyncness(&self) -> TokenStream2 {
603        let sails_path = self.sails_path;
604        let service_type = &self.result_type;
605        quote!(< #service_type as  #sails_path::meta::ServiceMeta>::ASYNC )
606    }
607
608    fn service_const_route(&self) -> TokenStream2 {
609        let route_ident = &self.route_ident();
610        let ctor_route_bytes = self.encoded_route.as_slice();
611        let ctor_route_len = ctor_route_bytes.len();
612        quote!(
613            const #route_ident: [u8; #ctor_route_len] = [ #(#ctor_route_bytes),* ];
614        )
615    }
616
617    fn service_invocation(&self) -> TokenStream2 {
618        let route_ident = &self.route_ident();
619        let service_ctor_ident = self.ident;
620
621        quote! {
622            if input.starts_with(& #route_ident) {
623                let mut service = program_ref.#service_ctor_ident();
624                let is_async = service
625                    .check_asyncness(&input[#route_ident .len()..])
626                    .unwrap_or_else(|| {
627                        gstd::unknown_input_panic("Unknown call", &input[#route_ident .len()..])
628                    });
629                if is_async {
630                    gstd::message_loop(async move {
631                        service
632                            .try_handle_async(&input[#route_ident .len()..], |encoded_result, value| {
633                                gstd::msg::reply_bytes(encoded_result, value)
634                                    .expect("Failed to send output");
635                            })
636                            .await
637                            .unwrap_or_else(|| {
638                                gstd::unknown_input_panic("Unknown request", &input)
639                            });
640                    });
641                } else {
642                    service
643                        .try_handle(&input[#route_ident .len()..], |encoded_result, value| {
644                            gstd::msg::reply_bytes(encoded_result, value)
645                                .expect("Failed to send output");
646                        })
647                        .unwrap_or_else(|| gstd::unknown_input_panic("Unknown request", &input));
648                }
649            }
650        }
651    }
652
653    fn original_service_ctor_fn(&self) -> ImplItemFn {
654        let mut original_service_ctor_fn = self.impl_fn.clone();
655        let original_service_ctor_fn_ident = Ident::new(
656            &format!("__{}", original_service_ctor_fn.sig.ident),
657            original_service_ctor_fn.sig.ident.span(),
658        );
659        original_service_ctor_fn.attrs.clear();
660        original_service_ctor_fn.vis = Visibility::Inherited;
661        original_service_ctor_fn.sig.ident = original_service_ctor_fn_ident;
662        original_service_ctor_fn
663    }
664
665    fn wrapping_service_ctor_fn(&self, original_service_ctor_fn_ident: &Ident) -> ImplItemFn {
666        let sails_path = self.sails_path;
667        let service_type = &self.result_type;
668        let route_ident = &self.route_ident();
669        let unwrap_token = self.unwrap_result.then(|| quote!(.unwrap()));
670
671        let mut wrapping_service_ctor_fn = self.impl_fn.clone();
672        // Filter out `export  attribute
673        wrapping_service_ctor_fn
674            .attrs
675            .retain(|attr| export::parse_attr(attr).is_none());
676        wrapping_service_ctor_fn.sig.output = parse_quote!(
677            -> < #service_type as #sails_path::gstd::services::Service>::Exposure
678        );
679        wrapping_service_ctor_fn.block = parse_quote!({
680            let service = self. #original_service_ctor_fn_ident () #unwrap_token;
681            let exposure = < #service_type as #sails_path::gstd::services::Service>::expose(
682                service,
683                #route_ident .as_ref(),
684            );
685            exposure
686        });
687        wrapping_service_ctor_fn
688    }
689
690    fn ctor_branch_impl(
691        &self,
692        program_type_path: &TypePath,
693        input_ident: &Ident,
694        program_ident: &Ident,
695    ) -> TokenStream2 {
696        let handler_ident = self.ident;
697        let unwrap_token = self.unwrap_result.then(|| quote!(.unwrap()));
698        let handler_args = self
699            .params_idents()
700            .iter()
701            .map(|ident| quote!(request.#ident));
702        let params_struct_ident = &self.params_struct_ident;
703        let payable_check = {
704            #[cfg(feature = "ethexe")]
705            {
706                self.payable_check()
707            }
708            #[cfg(not(feature = "ethexe"))]
709            {
710                quote!()
711            }
712        };
713
714        let ctor_call_impl = if self.is_async() {
715            quote! {
716                gstd::message_loop(async move {
717                    let program = #program_type_path :: #handler_ident (#(#handler_args),*) .await #unwrap_token ;
718
719                    unsafe {
720                        #program_ident = Some(program);
721                    }
722                });
723            }
724        } else {
725            quote! {
726                let program = #program_type_path :: #handler_ident (#(#handler_args),*) #unwrap_token;
727                unsafe {
728                    #program_ident = Some(program);
729                }
730            }
731        };
732
733        quote!(
734            if let Ok(request) = meta_in_program::#params_struct_ident::decode_params( #input_ident) {
735                #payable_check
736                #ctor_call_impl
737            }
738        )
739    }
740
741    fn ctor_params_struct(&self, scale_codec_path: &Path, scale_info_path: &Path) -> TokenStream2 {
742        let sails_path = self.sails_path;
743        let params_struct_ident = &self.params_struct_ident;
744        let params_struct_members = self.params().map(|(ident, ty)| quote!(#ident: #ty));
745        let ctor_route_bytes = self.encoded_route.as_slice();
746        let is_async = self.is_async();
747
748        quote! {
749            #[derive(#sails_path ::Decode, #sails_path ::TypeInfo)]
750            #[codec(crate = #scale_codec_path )]
751            #[scale_info(crate = #scale_info_path )]
752            pub struct #params_struct_ident {
753                #(pub(super) #params_struct_members,)*
754            }
755
756            impl InvocationIo for #params_struct_ident {
757                const ROUTE: &'static [u8] = &[ #(#ctor_route_bytes),* ];
758                type Params = Self;
759                const ASYNC: bool = #is_async;
760            }
761        }
762    }
763
764    fn ctor_meta_variant(&self) -> TokenStream2 {
765        let ctor_route = Ident::new(self.route.as_str(), Span::call_site());
766        let ctor_docs_attrs = self
767            .impl_fn
768            .attrs
769            .iter()
770            .filter(|attr| attr.path().is_ident("doc"));
771        let params_struct_ident = &self.params_struct_ident;
772
773        quote! {
774            #( #ctor_docs_attrs )*
775            #ctor_route(#params_struct_ident)
776        }
777    }
778}
779
780#[cfg(test)]
781mod tests {
782    use super::*;
783    use quote::quote;
784
785    #[test]
786    fn gprogram_discovers_public_associated_functions_returning_self_or_the_type_as_ctors() {
787        let program_impl = syn::parse2(quote!(
788            impl MyProgram {
789                fn non_public_associated_func_returning_self() -> Self {}
790                fn non_public_associated_func_returning_type() -> MyProgram {}
791                fn non_public_associated_func_returning_smth() -> u32 {}
792                pub fn public_associated_func_returning_self() -> Self {}
793                pub fn public_associated_func_returning_type() -> MyProgram {}
794                pub fn public_associated_func_returning_smth() -> u32 {}
795                fn non_public_method_returning_self(&self) -> Self {}
796                fn non_public_method_returning_type(&self) -> MyProgram {}
797                fn non_public_method_returning_smth(&self) -> u32 {}
798                pub fn public_method_returning_self(&self) -> Self {}
799                pub fn public_method_returning_type(&self) -> MyProgram {}
800                pub fn public_method_returning_smth(&self) -> u32 {}
801            }
802        ))
803        .unwrap();
804
805        let sails_path = &sails_paths::sails_path_or_default(None);
806        let discovered_ctors = discover_program_ctors(&program_impl, sails_path)
807            .iter()
808            .map(|fn_builder| fn_builder.ident.to_string())
809            .collect::<Vec<_>>();
810
811        assert_eq!(discovered_ctors.len(), 2);
812        assert!(discovered_ctors.contains(&String::from("public_associated_func_returning_self")));
813        assert!(discovered_ctors.contains(&String::from("public_associated_func_returning_type")));
814    }
815
816    #[test]
817    fn gprogram_discovers_public_methods_with_self_ref_only_and_some_return_as_service_funcs() {
818        let program_impl = syn::parse2(quote!(
819            impl MyProgram {
820                fn non_public_associated_func_returning_smth() -> u32 {}
821                fn non_public_associated_func_returning_unit() {}
822                pub fn public_associated_func_returning_smth() -> MyProgram {}
823                pub fn public_associated_func_returning_unit() {}
824                fn non_public_method_returning_smth(&self) -> u32 {}
825                fn non_public_method_returning_unit(&self) {}
826                pub fn public_method_returning_smth(&self) -> u32 {}
827                pub fn public_method_returning_smth_with_other_params(&self, p1: u32) -> u32 {}
828                pub fn public_methos_returning_smth_and_consuming_self(self) -> u32 {}
829            }
830        ))
831        .unwrap();
832
833        let sails_path = &sails_paths::sails_path_or_default(None);
834        let discovered_services =
835            shared::discover_invocation_targets(&program_impl, service_ctor_predicate, sails_path)
836                .iter()
837                .map(|fn_builder| fn_builder.ident.to_string())
838                .collect::<Vec<_>>();
839
840        assert_eq!(discovered_services.len(), 1);
841        assert!(discovered_services.contains(&String::from("public_method_returning_smth")));
842    }
843}