Skip to main content

sails_macros_core/program/
mod.rs

1use crate::{
2    export, sails_paths,
3    shared::{self, FnBuilder},
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    has_default_ctor_only: bool,
77}
78
79impl ProgramBuilder {
80    fn new(program_impl: ItemImpl, program_args: ProgramArgs) -> Self {
81        let mut program_impl = program_impl;
82        let type_constraints = program_impl.generics.where_clause.take();
83        let has_default_ctor_only = ensure_default_program_ctor(&mut program_impl);
84
85        Self {
86            program_impl,
87            program_args,
88            type_constraints,
89            has_default_ctor_only,
90        }
91    }
92
93    fn sails_path(&self) -> &Path {
94        self.program_args.sails_path()
95    }
96
97    fn impl_type(&self) -> (&TypePath, &PathArguments, &Ident) {
98        shared::impl_type_refs(self.program_impl.self_ty.as_ref())
99    }
100
101    fn impl_constraints(&self) -> (&Generics, Option<&WhereClause>) {
102        (&self.program_impl.generics, self.type_constraints.as_ref())
103    }
104
105    fn program_ctors(&self) -> Vec<FnBuilder<'_>> {
106        discover_program_ctors(&self.program_impl, self.sails_path())
107    }
108
109    fn handle_reply_fn(&mut self) -> Option<&mut ImplItemFn> {
110        let mut fn_iter = self.program_impl.items.iter_mut().filter_map(|item| {
111            if let ImplItem::Fn(fn_item) = item
112                && has_handle_reply_attr(fn_item) {
113                    fn_item
114                        .attrs
115                        .retain(|attr| !attr.path().is_ident("handle_reply"));
116                    if handle_reply_predicate(fn_item) {
117                        return Some(fn_item);
118                    } else {
119                        abort!(
120                            fn_item,
121                            "`handle_reply` function must be private and have a single `&self` argument and no return type"
122                        );
123                    }
124                }
125            None
126        });
127        let handle_reply_fn = fn_iter.next();
128        if let Some(duplicate) = fn_iter.next() {
129            abort!(duplicate, "only one `handle_reply` function is allowed");
130        }
131        handle_reply_fn
132    }
133
134    #[cfg(feature = "ethexe")]
135    fn service_ctors(&self) -> Vec<FnBuilder<'_>> {
136        shared::discover_invocation_targets(self, service_ctor_predicate, self.sails_path())
137    }
138}
139
140impl ProgramBuilder {
141    fn wire_up_service_exposure(
142        &mut self,
143        program_ident: &Ident,
144    ) -> (TokenStream2, TokenStream2, TokenStream2, TokenStream2) {
145        let mut services_meta = Vec::new();
146        let mut meta_asyncness = 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        // Collect all data we need in one pass without holding borrows
162        let mut modifications = Vec::new();
163        let mut services_count_data = Vec::new(); // Store data for count_base_services calls
164        let mut services_ids_data = Vec::new(); // Store data for AnyServiceIds
165        let mut route_dispatch_data = Vec::new(); // Store data for route dispatches
166
167        for (idx, impl_item) in self.program_impl.items.iter().enumerate() {
168            if let ImplItem::Fn(fn_item) = impl_item {
169                if is_mut_service_ctor(fn_item) {
170                    abort!(
171                        fn_item,
172                        "service constructor must take `&self`, not `&mut self`: a `&mut self` \
173                         factory can leak an exclusive borrow of program state across an `.await`"
174                    );
175                }
176
177                if !service_ctor_predicate(fn_item) {
178                    continue;
179                }
180
181                let mut invocation_export = shared::invocation_export_or_default(fn_item);
182                let entry_id = routes.len() as u16;
183
184                #[cfg(feature = "ethexe")]
185                {
186                    use convert_case::{Case, Casing};
187                    let camel_case_route = invocation_export.route.to_case(Case::Camel);
188                    shared::validation::validate_identifier(
189                        &camel_case_route,
190                        fn_item.sig.ident.span(),
191                        "Exposed Service",
192                    );
193                }
194                if let Some(duplicate) = routes.insert(
195                    invocation_export.route.clone(),
196                    fn_item.sig.ident.to_string(),
197                ) {
198                    abort!(
199                        invocation_export.span,
200                        "`export` attribute conflicts with one already assigned to '{}'",
201                        duplicate
202                    );
203                }
204
205                invocation_export.export = true;
206
207                let fn_builder =
208                    FnBuilder::new(invocation_export, entry_id, fn_item, self.sails_path());
209
210                let original_service_ctor_fn = fn_builder.original_service_ctor_fn();
211                let wrapping_service_ctor_fn =
212                    fn_builder.wrapping_service_ctor_fn(&original_service_ctor_fn.sig.ident);
213
214                services_meta.push(fn_builder.service_meta());
215
216                if !has_async_ctor {
217                    // If there are no async constructors, we can't push the asyncness as false,
218                    // as there could be async handlers in services.
219                    meta_asyncness.push(fn_builder.service_meta_asyncness());
220                }
221
222                #[cfg(feature = "ethexe")]
223                if fn_builder.has_ethabi_codec() {
224                    solidity_dispatchers.push(fn_builder.sol_service_invocation());
225                }
226
227                // Extract data needed later (not the fn_builder itself)
228                let service_type = fn_builder.result_type.clone();
229                let service_ctor_ident = fn_builder.ident.clone();
230
231                services_count_data.push(service_type.clone());
232                services_ids_data.push(service_type.clone());
233                if fn_builder.has_scale_codec() {
234                    route_dispatch_data.push((
235                        fn_builder.service_route_idx(),
236                        service_ctor_ident,
237                        service_type,
238                    ));
239                }
240
241                modifications.push((idx, original_service_ctor_fn, wrapping_service_ctor_fn));
242            }
243        }
244
245        // Apply modifications to self - no more borrows from items after this point
246        for (idx, original_service_ctor_fn, wrapping_service_ctor_fn) in modifications {
247            self.program_impl.items[idx] = ImplItem::Fn(original_service_ctor_fn);
248            self.program_impl
249                .items
250                .push(ImplItem::Fn(wrapping_service_ctor_fn));
251        }
252
253        if meta_asyncness.is_empty() {
254            meta_asyncness.push(quote!(false));
255        }
256
257        let handle_reply_fn = self.handle_reply_fn().map(|item_fn| {
258            let handle_reply_fn_ident = &item_fn.sig.ident;
259            quote! {
260                let program_ref = unsafe { #program_ident.as_ref() }.expect("Program not initialized");
261                program_ref.#handle_reply_fn_ident();
262            }
263        })
264        .unwrap_or_default();
265
266        let handle_signal_fn = self
267            .program_args
268            .handle_signal()
269            .map(|handle_signal_path| quote!( #handle_signal_path ();))
270            .unwrap_or_default();
271
272        let sails_path = self.sails_path();
273        let (program_type_path, _program_type_args, _) = self.impl_type();
274        let (generics, program_type_constraints) = self.impl_constraints();
275
276        let program_meta_impl = quote! {
277            impl #generics #sails_path::meta::ProgramMeta for #program_type_path #program_type_constraints {
278                type ConstructorsMeta = meta_in_program::ConstructorsMeta;
279
280                const SERVICES: &'static [(&'static str, #sails_path::meta::AnyServiceMeta)] = &[
281                    #(#services_meta),*
282                ];
283                const ASYNC: bool = #( #meta_asyncness )||*;
284            }
285        };
286
287        // Generate the INTERFACE_IDS registry using extracted data
288        let services_count_expr = {
289            let count_exprs = services_count_data.iter().map(|service_type| {
290                quote! {
291                    + #sails_path::meta::count_base_services::<#service_type>()
292                }
293            });
294
295            let base_count = services_count_data.len();
296
297            quote! {
298                const SERVICES_COUNT: usize = #base_count #(#count_exprs)*;
299            }
300        };
301
302        let services_ids_expr = {
303            let ids_exprs = services_ids_data.iter().map(|service_type| {
304                quote! {
305                    #sails_path::meta::BaseServiceMeta::new::<#service_type>(""),
306                }
307            });
308
309            quote! {
310                const INTERFACE_IDS: &'static [(#sails_path::meta::InterfaceId, u8)] =
311                    &#sails_path::meta::interface_ids::<SERVICES_COUNT>(&[
312                        #(#ids_exprs)*
313                    ]);
314            }
315        };
316
317        // Generate route_id match arms using extracted data
318        let route_dispatches = route_dispatch_data
319            .iter()
320            .map(|(route_idx, service_ctor_ident, service_type)| {
321                quote! {
322                    #route_idx => {
323                        let svc = program_ref.#service_ctor_ident();
324                        #sails_path::service_route_dispatch!(
325                            svc: #service_type,
326                            interface_id = interface_id,
327                            entry_id = entry_id,
328                            input = &input[header_len..],
329                        );
330                    }
331                }
332            })
333            .collect::<Vec<_>>();
334
335        let solidity_main = self.sol_main(solidity_dispatchers.as_slice());
336
337        let payable = self.program_args.payable().then(|| {
338            quote! {
339                if gstd::msg::value() > 0 && gstd::msg::size() == 0 {
340                    return;
341                }
342            }
343        });
344
345        let main_fn = quote!(
346            #services_count_expr
347            #services_ids_expr
348
349            #[unsafe(no_mangle)]
350            extern "C" fn handle() {
351                #payable
352
353                let mut input = gstd::msg::load_bytes().expect("Failed to read input");
354
355                let program_ref = unsafe { #program_ident.as_ref() }.expect("Program not initialized");
356
357                #solidity_main
358
359                if let Ok(header) = <#sails_path::meta::SailsMessageHeader as #sails_path::Decode>::decode(&mut input.as_slice()) {
360                    let header_len = header.hlen().inner() as usize;
361                    let (interface_id, route_id, entry_id) = header
362                        .try_match_interfaces(INTERFACE_IDS)
363                        .expect("Failed to find matching service")
364                        .into_inner();
365                    match route_id {
366                        #(#route_dispatches)*
367                        _ => gstd::unknown_input_panic("Unknown route_id", &[route_id])
368                    }
369                }
370            }
371        );
372
373        let handle_reply_fn = quote! {
374            #[unsafe(no_mangle)]
375            extern "C" fn handle_reply() {
376                use #sails_path::meta::ProgramMeta;
377
378                if #program_type_path::ASYNC {
379                    gstd::handle_reply_with_hook();
380                }
381
382                #handle_reply_fn
383            }
384        };
385
386        #[cfg(not(feature = "ethexe"))]
387        let handle_signal_fn = quote! {
388            #[unsafe(no_mangle)]
389            extern "C" fn handle_signal() {
390                use #sails_path::meta::ProgramMeta;
391
392                if #program_type_path::ASYNC {
393                    gstd::handle_signal();
394                }
395
396                #handle_signal_fn
397            }
398        };
399
400        (
401            program_meta_impl,
402            main_fn,
403            handle_reply_fn,
404            handle_signal_fn,
405        )
406    }
407
408    fn generate_init(&self, program_ident: &Ident) -> (TokenStream2, TokenStream2) {
409        let sails_path = self.sails_path();
410        let type_info_path = sails_paths::type_info_path(sails_path);
411
412        let (program_type_path, ..) = self.impl_type();
413        let input_ident = Ident::new("input", Span::call_site());
414
415        let program_ctors = self.program_ctors();
416
417        let mut ctor_dispatches = Vec::with_capacity(program_ctors.len() + 1);
418        let mut ctor_params_structs = Vec::with_capacity(program_ctors.len());
419        let mut ctor_meta_variants = Vec::with_capacity(program_ctors.len());
420
421        for fn_builder in &program_ctors {
422            if fn_builder.has_scale_codec() {
423                ctor_dispatches.push(fn_builder.ctor_branch_impl(
424                    program_type_path,
425                    &input_ident,
426                    program_ident,
427                ));
428            }
429            ctor_params_structs.push(fn_builder.ctor_params_struct());
430            ctor_meta_variants.push(fn_builder.ctor_meta_variant());
431        }
432
433        let solidity_init = self.sol_init(&input_ident);
434
435        let sails_init = quote! {
436            if let Ok(header) = <#sails_path::meta::SailsMessageHeader as #sails_path::Decode>::decode(&mut #input_ident) {
437                if header.interface_id() != #sails_path::meta::InterfaceId::zero() {
438                    #sails_path::gstd::unknown_input_panic("Non zero ctor interface_id", header.to_bytes().as_slice());
439                }
440                match header.entry_id() {
441                    #(#ctor_dispatches)*
442                    _ => #sails_path::gstd::unknown_input_panic("Unexpected ctor entry_id", input),
443                }
444            }
445        };
446
447        // For programs with no user-defined constructors, also accept an empty payload.
448        // The default create() takes no arguments and is idempotent, so an empty slice is
449        // unambiguous: it cannot be confused with any real constructor message.
450        let empty_input_guard = if self.has_default_ctor_only {
451            let fn_builder = program_ctors
452                .first()
453                .expect("default ctor must exist when has_default_ctor_only is true");
454            let ctor_ident = fn_builder.ident;
455            let params_struct_ident = &fn_builder.params_struct_ident;
456            quote! {
457                if #input_ident.is_empty() {
458                    #sails_path::program_ctor!(
459                        #program_ident = #program_type_path :: #ctor_ident (),
460                        params_struct = meta_in_program::#params_struct_ident
461                    );
462                    return;
463                }
464            }
465        } else {
466            quote!()
467        };
468
469        let init_fn = quote! {
470            #[unsafe(no_mangle)]
471            extern "C" fn init() {
472                let mut #input_ident: &[u8] = &gstd::msg::load_bytes().expect("Failed to read input");
473
474                #empty_input_guard
475
476                #solidity_init
477
478                #sails_init
479            }
480        };
481
482        let meta_in_program = quote! {
483            mod meta_in_program {
484                use super::*;
485
486                #( #ctor_params_structs )*
487
488                #[derive(#sails_path::TypeInfo)]
489                #[type_info(crate = #type_info_path)]
490                pub enum ConstructorsMeta {
491                    #( #ctor_meta_variants ),*
492                }
493            }
494        };
495        (meta_in_program, init_fn)
496    }
497}
498
499// Empty ProgramBuilder Implementations without `ethexe` feature
500#[cfg(not(feature = "ethexe"))]
501impl ProgramBuilder {
502    fn program_signature_impl(&self) -> TokenStream2 {
503        quote!()
504    }
505
506    fn match_ctor_impl(&self, _program_ident: &Ident) -> TokenStream2 {
507        quote!()
508    }
509
510    fn program_const(&self) -> TokenStream2 {
511        quote!()
512    }
513
514    fn sol_init(&self, _input_ident: &Ident) -> TokenStream2 {
515        quote!()
516    }
517
518    fn sol_main(&self, _solidity_dispatchers: &[TokenStream2]) -> TokenStream2 {
519        quote!()
520    }
521}
522
523impl Deref for ProgramBuilder {
524    type Target = ItemImpl;
525
526    fn deref(&self) -> &Self::Target {
527        &self.program_impl
528    }
529}
530
531impl DerefMut for ProgramBuilder {
532    fn deref_mut(&mut self) -> &mut Self::Target {
533        &mut self.program_impl
534    }
535}
536
537fn gen_gprogram_impl(program_impl: ItemImpl, program_args: ProgramArgs) -> TokenStream2 {
538    let mut program_builder = ProgramBuilder::new(program_impl, program_args);
539
540    let sails_path = program_builder.sails_path().clone();
541
542    let program_ident = Ident::new("PROGRAM", Span::call_site());
543
544    // Call this before `wire_up_service_exposure`
545    let program_signature_impl = program_builder.program_signature_impl();
546    let match_ctor_impl = program_builder.match_ctor_impl(&program_ident);
547    let program_const = program_builder.program_const();
548
549    let (program_meta_impl, main_fn, handle_reply_fn, handle_signal_fn) =
550        program_builder.wire_up_service_exposure(&program_ident);
551    let (meta_in_program, init_fn) = program_builder.generate_init(&program_ident);
552
553    let (program_type_path, ..) = program_builder.impl_type();
554
555    let program_impl = program_builder.deref();
556
557    quote!(
558        #program_impl
559
560        #program_meta_impl
561
562        #meta_in_program
563
564        #program_signature_impl
565
566        #program_const
567
568        #[cfg(target_arch = "wasm32")]
569        pub mod wasm {
570            use super::*;
571            use #sails_path::{gstd, hex, prelude::*};
572
573            static mut #program_ident: Option<#program_type_path> = None;
574
575            #init_fn
576
577            #match_ctor_impl
578
579            #main_fn
580
581            #handle_reply_fn
582
583            #handle_signal_fn
584        }
585    )
586}
587
588fn ensure_default_program_ctor(program_impl: &mut ItemImpl) -> bool {
589    let sails_path = &sails_paths::sails_path_or_default(None);
590    if discover_program_ctors(program_impl, sails_path).is_empty() {
591        program_impl.items.push(ImplItem::Fn(parse_quote!(
592            pub fn create() -> Self {
593                Default::default()
594            }
595        )));
596        return true;
597    }
598    false
599}
600
601fn discover_program_ctors<'a>(
602    program_impl: &'a ItemImpl,
603    sails_path: &'a Path,
604) -> Vec<FnBuilder<'a>> {
605    let self_type_path: TypePath = parse_quote!(Self);
606    let (program_type_path, _, _) = shared::impl_type_refs(program_impl.self_ty.as_ref());
607    let mut vec = shared::discover_invocation_targets(
608        program_impl,
609        |fn_item| program_ctor_predicate(fn_item, &self_type_path, program_type_path),
610        sails_path,
611    );
612    vec.sort_by_key(|f| f.route.to_lowercase());
613    vec.iter_mut()
614        .enumerate()
615        .for_each(|(idx, f)| f.entry_id = idx as u16);
616
617    #[cfg(feature = "ethexe")]
618    {
619        for ctor in &vec {
620            shared::validation::validate_identifier(
621                &ctor.route_camel_case(),
622                ctor.ident.span(),
623                "Program constructor",
624            );
625        }
626    }
627    vec
628}
629
630fn program_ctor_predicate(
631    fn_item: &ImplItemFn,
632    self_type_path: &TypePath,
633    program_type_path: &TypePath,
634) -> bool {
635    if matches!(fn_item.vis, Visibility::Public(_))
636        && fn_item.sig.receiver().is_none()
637        && let ReturnType::Type(_, output_type) = &fn_item.sig.output
638        && let Type::Path(output_type_path) = output_type.as_ref()
639    {
640        if output_type_path == self_type_path || output_type_path == program_type_path {
641            return true;
642        }
643        if let Some((Type::Path(output_type_path), _)) =
644            shared::extract_result_types(output_type_path)
645            && (output_type_path == self_type_path || output_type_path == program_type_path)
646        {
647            return true;
648        }
649    }
650    false
651}
652
653fn service_ctor_receiver(fn_item: &ImplItemFn) -> Option<&Receiver> {
654    fn_item.sig.receiver().filter(|receiver| {
655        matches!(fn_item.vis, Visibility::Public(_))
656            && fn_item.sig.inputs.len() == 1
657            && !matches!(fn_item.sig.output, ReturnType::Default)
658            && receiver.reference.is_some()
659    })
660}
661
662fn service_ctor_predicate(fn_item: &ImplItemFn) -> bool {
663    service_ctor_receiver(fn_item).is_some_and(|receiver| receiver.mutability.is_none())
664}
665
666/// Detects a public method shaped like a service constructor but taking `&mut self`.
667/// Used to emit a clear compile error instead of silently ignoring such a method
668/// (which `service_ctor_predicate` rejects).
669fn is_mut_service_ctor(fn_item: &ImplItemFn) -> bool {
670    service_ctor_receiver(fn_item).is_some_and(|receiver| receiver.mutability.is_some())
671}
672
673fn has_handle_reply_attr(fn_item: &ImplItemFn) -> bool {
674    fn_item
675        .attrs
676        .iter()
677        .any(|attr| attr.path().is_ident("handle_reply"))
678}
679
680fn handle_reply_predicate(fn_item: &ImplItemFn) -> bool {
681    matches!(fn_item.vis, Visibility::Inherited)
682        && matches!(
683            fn_item.sig.receiver(),
684            Some(Receiver {
685                mutability: None,
686                reference: Some(_),
687                ..
688            })
689        )
690        && fn_item.sig.inputs.len() == 1
691        && matches!(fn_item.sig.output, ReturnType::Default)
692}
693
694impl FnBuilder<'_> {
695    fn service_route_idx(&self) -> u8 {
696        if self.entry_id >= u8::MAX as u16 {
697            abort!(self.ident, "too many services; maximum is 255");
698        }
699        (self.entry_id + 1) as u8
700    }
701
702    fn service_meta(&self) -> TokenStream2 {
703        let sails_path = self.sails_path;
704        let route = &self.route;
705        let service_type = &self.result_type;
706        quote!(
707            ( #route , < #service_type as #sails_path::meta::ServiceMeta >::META )
708        )
709    }
710
711    fn service_meta_asyncness(&self) -> TokenStream2 {
712        let sails_path = self.sails_path;
713        let service_type = &self.result_type;
714        quote!(<#service_type as #sails_path::meta::ServiceMeta>::ASYNC )
715    }
716
717    fn original_service_ctor_fn(&self) -> ImplItemFn {
718        let mut original_service_ctor_fn = self.impl_fn.clone();
719        let original_service_ctor_fn_ident = Ident::new(
720            &format!("__{}", original_service_ctor_fn.sig.ident),
721            original_service_ctor_fn.sig.ident.span(),
722        );
723        original_service_ctor_fn.attrs.clear();
724        original_service_ctor_fn.vis = Visibility::Inherited;
725        original_service_ctor_fn.sig.ident = original_service_ctor_fn_ident;
726        original_service_ctor_fn
727    }
728
729    fn wrapping_service_ctor_fn(&self, original_service_ctor_fn_ident: &Ident) -> ImplItemFn {
730        let sails_path = self.sails_path;
731        let service_type = &self.result_type;
732        let route_idx = self.service_route_idx();
733        let unwrap_token = self.error_type.is_some().then(|| quote!(.unwrap()));
734
735        let mut wrapping_service_ctor_fn = self.impl_fn.clone();
736        // Filter out `export  attribute
737        wrapping_service_ctor_fn
738            .attrs
739            .retain(|attr| export::parse_attr(attr).is_none());
740        wrapping_service_ctor_fn.sig.output = parse_quote!(
741            -> < #service_type as #sails_path::gstd::services::Service>::Exposure
742        );
743        wrapping_service_ctor_fn.block = parse_quote!({
744            let service = self. #original_service_ctor_fn_ident () #unwrap_token;
745            let exposure = < #service_type as #sails_path::gstd::services::Service>::expose(
746                service,
747                #route_idx,
748            );
749            exposure
750        });
751        wrapping_service_ctor_fn
752    }
753
754    fn ctor_branch_impl(
755        &self,
756        program_type_path: &TypePath,
757        input_ident: &Ident,
758        program_ident: &Ident,
759    ) -> TokenStream2 {
760        let sails_path = self.sails_path;
761        let handler_ident = self.ident;
762        let entry_id = self.entry_id;
763        let handler_args = self.params_idents();
764        let handler_types = self.params_types();
765        let payable_check = {
766            #[cfg(feature = "ethexe")]
767            {
768                self.payable_check()
769            }
770            #[cfg(not(feature = "ethexe"))]
771            {
772                quote!()
773            }
774        };
775
776        let await_token = self.is_async().then(|| quote!(.await));
777        let unwrap_token = self.error_type.is_some().then(|| quote!(.unwrap()));
778        let raw_call = quote! { #program_type_path :: #handler_ident (#(#handler_args),*) #await_token #unwrap_token };
779        let params_struct_ident = &self.params_struct_ident;
780
781        let ctor_call_impl = quote! {
782            #sails_path::program_ctor!(
783                #program_ident = #raw_call,
784                params_struct = meta_in_program::#params_struct_ident
785            )
786        };
787
788        quote!(
789            #entry_id => {
790                let (#(#handler_args),*): (#(#handler_types),*)  = #sails_path::Decode::decode(&mut #input_ident)
791                    .unwrap_or_else(|_| #sails_path::gstd::unknown_input_panic("Unknown request", #input_ident));
792                #payable_check
793                #ctor_call_impl
794            }
795        )
796    }
797
798    fn ctor_params_struct(&self) -> TokenStream2 {
799        let sails_path = self.sails_path;
800        let params_struct_ident = &self.params_struct_ident;
801        let params_struct_members = self.params().map(|(ident, ty)| quote!(#ident: #ty));
802        let entry_id = &self.entry_id;
803        let decode_disabled = (!self.has_scale_codec()).then(|| quote!(decode = false,));
804
805        quote! {
806            #sails_path::invocation_io!(
807                pub struct #params_struct_ident {
808                    #(pub(super) #params_struct_members,)*
809                },
810                entry_id = #entry_id,
811                #decode_disabled
812            );
813        }
814    }
815
816    fn ctor_meta_variant(&self) -> TokenStream2 {
817        let ctor_route = Ident::new(self.route.as_str(), Span::call_site());
818        let ctor_docs_attrs = self
819            .impl_fn
820            .attrs
821            .iter()
822            .filter(|attr| attr.path().is_ident("doc"));
823        let params_struct_ident = &self.params_struct_ident;
824
825        #[cfg(feature = "ethexe")]
826        let payable_ann = self.payable.then(|| quote!(#[annotate(payable)]));
827        #[cfg(not(feature = "ethexe"))]
828        let payable_ann: Option<TokenStream2> = None;
829
830        #[cfg(feature = "ethexe")]
831        let codec_ann: Option<TokenStream2> =
832            match (self.has_scale_codec(), self.has_ethabi_codec()) {
833                (true, false) => Some(quote!(#[annotate(codec = "scale")])),
834                (false, true) => Some(quote!(#[annotate(codec = "ethabi")])),
835                _ => None,
836            };
837        #[cfg(not(feature = "ethexe"))]
838        let codec_ann: Option<TokenStream2> = None;
839
840        if let Some(err_ty) = &self.error_type {
841            let err_ty = shared::replace_any_lifetime_with_static(err_ty.clone());
842            quote! {
843                #( #ctor_docs_attrs )*
844                #payable_ann
845                #codec_ann
846                #ctor_route(#params_struct_ident, #err_ty)
847            }
848        } else {
849            quote! {
850                #( #ctor_docs_attrs )*
851                #payable_ann
852                #codec_ann
853                #ctor_route(#params_struct_ident)
854            }
855        }
856    }
857}
858
859#[cfg(test)]
860mod tests {
861    use super::*;
862    use quote::quote;
863
864    #[test]
865    fn gprogram_discovers_public_associated_functions_returning_self_or_the_type_as_ctors() {
866        let program_impl = syn::parse2(quote!(
867            impl MyProgram {
868                fn non_public_associated_func_returning_self() -> Self {}
869                fn non_public_associated_func_returning_type() -> MyProgram {}
870                fn non_public_associated_func_returning_smth() -> u32 {}
871                pub fn public_associated_func_returning_self() -> Self {}
872                pub fn public_associated_func_returning_type() -> MyProgram {}
873                pub fn public_associated_func_returning_smth() -> u32 {}
874                fn non_public_method_returning_self(&self) -> Self {}
875                fn non_public_method_returning_type(&self) -> MyProgram {}
876                fn non_public_method_returning_smth(&self) -> u32 {}
877                pub fn public_method_returning_self(&self) -> Self {}
878                pub fn public_method_returning_type(&self) -> MyProgram {}
879                pub fn public_method_returning_smth(&self) -> u32 {}
880            }
881        ))
882        .unwrap();
883
884        let sails_path = &sails_paths::sails_path_or_default(None);
885        let discovered_ctors = discover_program_ctors(&program_impl, sails_path)
886            .iter()
887            .map(|fn_builder| fn_builder.ident.to_string())
888            .collect::<Vec<_>>();
889
890        assert_eq!(discovered_ctors.len(), 2);
891        assert!(discovered_ctors.contains(&String::from("public_associated_func_returning_self")));
892        assert!(discovered_ctors.contains(&String::from("public_associated_func_returning_type")));
893    }
894
895    #[test]
896    fn gprogram_discovers_public_methods_with_self_ref_only_and_some_return_as_service_funcs() {
897        let program_impl = syn::parse2(quote!(
898            impl MyProgram {
899                fn non_public_associated_func_returning_smth() -> u32 {}
900                fn non_public_associated_func_returning_unit() {}
901                pub fn public_associated_func_returning_smth() -> MyProgram {}
902                pub fn public_associated_func_returning_unit() {}
903                fn non_public_method_returning_smth(&self) -> u32 {}
904                fn non_public_method_returning_unit(&self) {}
905                pub fn public_method_returning_smth(&self) -> u32 {}
906                pub fn public_method_returning_smth_with_other_params(&self, p1: u32) -> u32 {}
907                pub fn public_methos_returning_smth_and_consuming_self(self) -> u32 {}
908            }
909        ))
910        .unwrap();
911
912        let sails_path = &sails_paths::sails_path_or_default(None);
913        let discovered_services =
914            shared::discover_invocation_targets(&program_impl, service_ctor_predicate, sails_path)
915                .iter()
916                .map(|fn_builder| fn_builder.ident.to_string())
917                .collect::<Vec<_>>();
918
919        assert_eq!(discovered_services.len(), 1);
920        assert!(discovered_services.contains(&String::from("public_method_returning_smth")));
921    }
922
923    #[test]
924    fn is_mut_service_ctor_detects_only_public_mut_self_factories() {
925        let program_impl: ItemImpl = syn::parse2(quote!(
926            impl MyProgram {
927                pub fn mut_factory(&mut self) -> MyService {}
928                pub fn shared_factory(&self) -> MyService {}
929                fn non_public_mut_factory(&mut self) -> MyService {}
930                pub fn mut_factory_returning_unit(&mut self) {}
931                pub fn mut_factory_with_other_params(&mut self, p1: u32) -> MyService {}
932            }
933        ))
934        .unwrap();
935
936        let detected = program_impl
937            .items
938            .iter()
939            .filter_map(|item| match item {
940                ImplItem::Fn(fn_item) if is_mut_service_ctor(fn_item) => {
941                    Some(fn_item.sig.ident.to_string())
942                }
943                _ => None,
944            })
945            .collect::<Vec<_>>();
946
947        assert_eq!(detected, vec![String::from("mut_factory")]);
948    }
949}