sails-client-gen-v2 1.0.0-beta.5

Rust client generator for the Sails framework
Documentation
use crate::events_generator::EventsModuleGenerator;
use crate::helpers::*;
use crate::mock_generator::MockGenerator;
use crate::type_generators::{TopLevelTypeGenerator, generate_type_decl_with_path};
use convert_case::{Case, Casing};
use genco::prelude::*;
use rust::Tokens;
use sails_idl_ast::codec::has_scale_codec;
use sails_idl_parser_v2::{ast, visitor, visitor::Visitor};
use std::collections::HashMap;

/// Generates a service module with trait and struct implementation
pub(crate) struct ServiceGenerator<'ast> {
    service_name: &'ast str,
    sails_path: &'ast str,
    external_types: &'ast HashMap<&'ast str, &'ast str>,
    mocks_feature_name: Option<&'ast str>,
    trait_tokens: Tokens,
    impl_tokens: Tokens,
    io_tokens: Tokens,
    events_tokens: Tokens,
    types_tokens: Tokens,
    mocks_tokens: Tokens,
    interface_id: sails_idl_ast::InterfaceId,
    no_derive_traits: bool,
}

impl<'ast> ServiceGenerator<'ast> {
    pub(crate) fn new(
        service_name: &'ast str,
        sails_path: &'ast str,
        external_types: &'ast HashMap<&'ast str, &'ast str>,
        mocks_feature_name: Option<&'ast str>,
        interface_id: sails_idl_ast::InterfaceId,
        no_derive_traits: bool,
    ) -> Self {
        Self {
            service_name,
            sails_path,
            external_types,
            mocks_feature_name,
            trait_tokens: Tokens::new(),
            impl_tokens: Tokens::new(),
            io_tokens: Tokens::new(),
            events_tokens: Tokens::new(),
            types_tokens: Tokens::new(),
            mocks_tokens: Tokens::new(),
            interface_id,
            no_derive_traits,
        }
    }

    pub(crate) fn finalize(self) -> Tokens {
        let service_name_snake = &self.service_name.to_case(Case::Snake);
        let mock_tokens = if let Some(mocks_feature_name) = self.mocks_feature_name {
            quote! {
                $['\n']
                #[cfg(feature = $(quoted(mocks_feature_name)))]
                #[cfg(not(target_arch = "wasm32"))]
                pub mod mockall {
                    use super::*;
                    use $(self.sails_path)::mockall::*;
                    $(self.mocks_tokens)
                }
            }
        } else {
            quote!()
        };

        let bytes = self.interface_id.as_bytes().iter().copied();
        let interface_id_tokens = quote! {
            const INTERFACE_ID: $(self.sails_path)::InterfaceId = $(self.sails_path)::InterfaceId::from_bytes_8([ $(for b in bytes join (, ) => $b) ]);
        };

        let has_io = !self.io_tokens.is_empty();
        let has_events = !self.events_tokens.is_empty();
        let has_types = !self.types_tokens.is_empty();

        let io_module = if has_io {
            quote! {
                $['\n']
                pub mod io {
                    use super::*;
                    $(self.io_tokens)
                }
            }
        } else {
            quote!()
        };

        let allow_unused = if !has_io && !has_events && !has_types {
            quote!(#[allow(unused_imports)])
        } else {
            quote!()
        };

        quote! {
            $['\n']
            $allow_unused
            pub mod $service_name_snake {
                use super::*;

                $(self.types_tokens)

                pub trait $(self.service_name) {
                    type Env: $(self.sails_path)::client::GearEnv;
                    $(self.trait_tokens)
                }

                pub struct $(self.service_name)Impl;

                impl $(self.sails_path)::client::Identifiable for $(self.service_name)Impl {
                    $interface_id_tokens
                }

                impl<E: $(self.sails_path)::client::GearEnv> $(self.service_name) for $(self.sails_path)::client::Service<$(self.service_name)Impl, E> {
                    type Env = E;
                    $(self.impl_tokens)
                }

                $io_module

                $(self.events_tokens)

                $(mock_tokens)
            }
        }
    }
}

// using quote_in instead of tokens.append
impl<'ast> Visitor<'ast> for ServiceGenerator<'ast> {
    fn visit_service_unit(&mut self, service: &'ast ast::ServiceUnit) {
        visitor::accept_service_unit(service, self);

        for service_ident in &service.extends {
            let name = &service_ident.name;
            let method_name = name.to_case(Case::Snake);
            let impl_name = name.to_case(Case::Pascal);
            let mod_name = name.to_case(Case::Snake);

            quote_in! { self.trait_tokens =>
                $['\r'] fn $(&method_name)(&self) -> $(self.sails_path)::client::Service<super::$(mod_name.as_str())::$(impl_name.as_str())Impl, Self::Env>;
            };

            quote_in! { self.impl_tokens =>
                $['\r'] fn $(&method_name)(&self) -> $(self.sails_path)::client::Service<super::$(mod_name.as_str())::$(impl_name.as_str())Impl, Self::Env> {
                    self.base_service()
                }
            };
        }

        let mut mock_gen = MockGenerator::new(self.service_name, self.sails_path);
        mock_gen.visit_service_unit(service);
        self.mocks_tokens.extend(mock_gen.finalize());

        if !service.events.is_empty() {
            let mut events_mod_gen = EventsModuleGenerator::new(self.service_name, self.sails_path);
            events_mod_gen.visit_service_unit(service);
            self.events_tokens = events_mod_gen.finalize();
        }
    }

    fn visit_type(&mut self, t: &'ast ast::Type) {
        if self.external_types.contains_key(t.name.as_str()) {
            return;
        }

        let mut type_gen =
            TopLevelTypeGenerator::new(&t.name, self.sails_path, self.no_derive_traits);
        type_gen.visit_type(t);
        self.types_tokens.extend(type_gen.finalize());
    }

    fn visit_service_func(&mut self, func: &'ast ast::ServiceFunc) {
        if !has_scale_codec(&func.annotations) {
            return;
        }
        let self_ref = if func.kind == ast::FunctionKind::Query {
            "&self"
        } else {
            "&mut self"
        };
        let fn_name = &func.name;
        let fn_name_snake = &fn_name.to_case(Case::Snake);

        let params_with_types = &fn_args_with_types_path(&func.params, "");
        let args = encoded_args(&func.params);

        generate_doc_comments(&mut self.trait_tokens, &func.docs);

        quote_in! { self.trait_tokens =>
            $['\r'] fn $fn_name_snake ($self_ref, $params_with_types) -> $(self.sails_path)::client::PendingCall<io::$fn_name, Self::Env>;
        };

        quote_in! {self.impl_tokens =>
            $['\r'] fn $fn_name_snake ($self_ref, $params_with_types) -> $(self.sails_path)::client::PendingCall<io::$fn_name, Self::Env> {
                self.pending_call($args)
            }
        };

        let io_output_type = if let Some(throws_type) = &func.throws {
            let ok_type = generate_type_decl_with_path(&func.output, "super");
            let err_type = generate_type_decl_with_path(throws_type, "super");
            format!("{ok_type} | {err_type}")
        } else {
            generate_type_decl_with_path(&func.output, "super")
        };

        let params_with_types_super = &fn_args_with_types_path(&func.params, "super");
        let entry_id = func.entry_id;

        quote_in! { self.io_tokens =>
            $(self.sails_path)::io_struct_impl!($fn_name ($params_with_types_super) -> $io_output_type, $entry_id, <super::$(self.service_name)Impl as $(self.sails_path)::client::Identifiable>::INTERFACE_ID);
        };
    }
}