use crate::{
sails_paths,
shared::{self, FnBuilder},
};
use args::ServiceArgs;
use convert_case::{Case, Casing};
use proc_macro_error::abort;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{Generics, Ident, ItemImpl, Path, Type, TypePath, Visibility, WhereClause};
mod args;
#[cfg(feature = "ethexe")]
mod ethexe;
mod exposure;
mod meta;
pub fn gservice(args: TokenStream, service_impl: TokenStream) -> TokenStream {
let service_impl = parse_gservice_impl(service_impl);
ensure_single_gservice_on_impl(&service_impl);
generate_gservice(args, service_impl)
}
#[doc(hidden)]
pub fn __gservice_internal(args: TokenStream, service_impl: TokenStream) -> TokenStream {
let service_impl = parse_gservice_impl(service_impl);
generate_gservice(args, service_impl)
}
fn parse_gservice_impl(service_impl_tokens: TokenStream) -> ItemImpl {
syn::parse2(service_impl_tokens).unwrap_or_else(|err| {
abort!(
err.span(),
"`service` attribute can be applied to impls only: {}",
err
)
})
}
fn ensure_single_gservice_on_impl(service_impl: &ItemImpl) {
let attr_gservice = service_impl.attrs.iter().find(|attr| {
attr.meta
.path()
.segments
.last()
.map(|s| s.ident == "service")
.unwrap_or(false)
});
if attr_gservice.is_some() {
abort!(
service_impl,
"multiple `service` attributes on the same impl are not allowed",
)
}
}
struct ServiceBuilder<'a> {
service_impl: &'a ItemImpl,
sails_path: &'a Path,
base_types: &'a [Path],
generics: Generics,
type_constraints: Option<WhereClause>,
type_path: &'a TypePath,
events_type: Option<&'a Path>,
service_handlers: Vec<FnBuilder<'a>>,
exposure_ident: Ident,
route_idx_ident: Ident,
inner_ident: Ident,
meta_module_ident: Ident,
}
#[derive(Clone, Copy)]
pub(crate) enum Codec {
Scale,
#[cfg(feature = "ethexe")]
Ethabi,
}
struct DispatchParams<'a> {
is_async: bool,
method_name_ident: &'a Ident,
method_sig: &'a TokenStream,
extra_imports: &'a TokenStream,
metadata_type: &'a TokenStream,
codec: Codec,
}
impl<'a> ServiceBuilder<'a> {
fn from(
service_impl: &'a ItemImpl,
sails_path: &'a Path,
service_args: &'a ServiceArgs,
) -> Self {
let (generics, type_constraints) = shared::impl_constraints(service_impl);
let (type_path, _type_args, service_ident) =
shared::impl_type_refs(service_impl.self_ty.as_ref());
let service_handlers = discover_service_handlers(service_impl, sails_path);
let exposure_name = format!(
"{}Exposure",
service_ident.to_string().to_case(Case::Pascal)
);
let exposure_ident = Ident::new(&exposure_name, Span::call_site());
let route_idx_ident = Ident::new("route_idx", Span::call_site());
let inner_ident = Ident::new("inner", Span::call_site());
let meta_module_name = format!("{}_meta", service_ident.to_string().to_case(Case::Snake));
let meta_module_ident = Ident::new(&meta_module_name, Span::call_site());
let base_types = service_args.base_types();
Self {
service_impl,
sails_path,
base_types,
generics,
type_constraints,
type_path,
events_type: service_args.events_type(),
service_handlers,
exposure_ident,
route_idx_ident,
inner_ident,
meta_module_ident,
}
}
fn type_constraints(&self) -> Option<&WhereClause> {
self.type_constraints.as_ref()
}
}
#[cfg(not(feature = "ethexe"))]
impl ServiceBuilder<'_> {
fn service_signature_impl(&self) -> TokenStream {
quote!()
}
fn try_handle_solidity_impl(&self) -> TokenStream {
quote!()
}
fn exposure_emit_eth_impls(&self) -> Option<TokenStream> {
None
}
}
fn generate_gservice(args: TokenStream, service_impl: ItemImpl) -> TokenStream {
let service_args = syn::parse2::<ServiceArgs>(args).unwrap_or_else(|err| {
abort!(
err.span(),
"failed to parse `service` attribute arguments: {}",
err
)
});
let sails_path = service_args.sails_path();
let service_builder = ServiceBuilder::from(&service_impl, &sails_path, &service_args);
if service_builder.service_handlers.is_empty() && service_builder.base_types.is_empty() {
abort!(
service_builder.service_impl,
"`service` attribute requires impl to define at least one public method with `#[export]` macro or extend another service"
);
}
let meta_module = service_builder.meta_module();
let exposure_struct = service_builder.exposure_struct();
let exposure_impl = service_builder.exposure_impl();
let service_trait_impl = service_builder.service_trait_impl();
let service_signature_impl = service_builder.service_signature_impl();
quote!(
#exposure_struct
#exposure_impl
#service_trait_impl
#meta_module
#service_signature_impl
)
}
fn discover_service_handlers<'a>(
service_impl: &'a ItemImpl,
sails_path: &'a Path,
) -> Vec<FnBuilder<'a>> {
let mut vec: Vec<_> = shared::discover_invocation_targets(
service_impl,
|fn_item| matches!(fn_item.vis, Visibility::Public(_)) && fn_item.sig.receiver().is_some(),
sails_path,
)
.into_iter()
.filter(|fn_builder| fn_builder.export)
.collect();
vec.sort_by_key(|f| f.route.to_lowercase());
vec.iter_mut()
.filter(|f| f.overrides.is_none())
.enumerate()
.for_each(|(idx, f)| f.entry_id = idx as u16);
vec
}
impl FnBuilder<'_> {
fn result_type_with_static_lifetime(&self) -> Type {
let (result_type, _) = self.result_type_with_value();
shared::replace_any_lifetime_with_static(result_type.clone())
}
fn handler_meta_variant(&self) -> TokenStream {
let handler_route_ident = Ident::new(self.route.as_str(), Span::call_site());
let handler_docs_attrs = self
.impl_fn
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"));
let params_struct_ident = &self.params_struct_ident;
let result_type = self.result_type_with_static_lifetime();
#[cfg(feature = "ethexe")]
let payable_ann = self.payable.then(|| quote!(#[annotate(payable)]));
#[cfg(not(feature = "ethexe"))]
let payable_ann: Option<TokenStream> = None;
let returns_value_ann = if cfg!(feature = "ethexe") {
self.result_type_with_value()
.1
.then(|| quote!(#[annotate(returns_value)]))
} else {
None
};
#[cfg(feature = "ethexe")]
let codec_ann: Option<TokenStream> = match (self.has_scale_codec(), self.has_ethabi_codec())
{
(true, false) => Some(quote!(#[annotate(codec = "scale")])),
(false, true) => Some(quote!(#[annotate(codec = "ethabi")])),
_ => None,
};
#[cfg(not(feature = "ethexe"))]
let codec_ann: Option<TokenStream> = None;
if let Some(err_ty) = &self.error_type {
let err_ty = shared::replace_any_lifetime_with_static(err_ty.clone());
quote!(
#( #handler_docs_attrs )*
#payable_ann
#returns_value_ann
#codec_ann
#handler_route_ident(#params_struct_ident, #result_type, #err_ty)
)
} else {
quote!(
#( #handler_docs_attrs )*
#payable_ann
#returns_value_ann
#codec_ann
#handler_route_ident(#params_struct_ident, #result_type)
)
}
}
fn params_struct(&self, own_interface_id: &TokenStream) -> TokenStream {
let sails_path = self.sails_path;
let params_struct_ident = &self.params_struct_ident;
let params_struct_members = self.params().map(|(ident, ty)| quote!(#ident: #ty));
let (interface_id_computation, entry_id_computation) = if let Some(base_path) =
&self.overrides
{
let base_path_wo_lifetimes = shared::remove_lifetimes(base_path);
let name = &self.route;
let entry_id_check = if let Some(id) = self.override_entry_id {
quote! { #id }
} else {
quote! {
{
const ID: u16 = #sails_path::meta::find_id(
<super::#base_path_wo_lifetimes as #sails_path::meta::ServiceMeta>::METHODS,
#name,
);
ID
}
}
};
(
quote! { <super::#base_path_wo_lifetimes as #sails_path::meta::Identifiable>::INTERFACE_ID },
entry_id_check,
)
} else {
let entry_id = &self.entry_id;
(quote! { #own_interface_id }, quote! { #entry_id })
};
let decode_disabled = (!self.has_scale_codec()).then(|| quote!(decode = false,));
quote!(
#sails_path::invocation_io!(
pub struct #params_struct_ident {
#(pub(super) #params_struct_members,)*
},
interface_id = #interface_id_computation,
entry_id = #entry_id_computation,
#decode_disabled
);
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
#[test]
fn discover_service_handlers_with_export() {
let service_impl = syn::parse2(quote!(
impl Service {
fn non_public_associated_func_returning_self() -> Self {}
fn non_public_associated_func_returning_type() -> Service {}
fn non_public_associated_func_returning_smth() -> u32 {}
pub fn public_associated_func_returning_self() -> Self {}
pub fn public_associated_func_returning_type() -> Service {}
pub fn public_associated_func_returning_smth() -> u32 {}
fn non_public_method_returning_self(&self) -> Self {}
fn non_public_method_returning_type(&self) -> Service {}
fn non_public_method_returning_smth(&self) -> u32 {}
pub fn public_method_returning_self(&self) -> Self {}
pub fn public_method_returning_type(&self) -> Service {}
pub fn public_method_returning_smth(&self) -> u32 {}
#[export]
pub fn export_public_method_returning_self(&self) -> Self {}
#[export]
pub fn export_public_method_returning_type(&self) -> Service {}
#[export]
pub fn export_public_method_returning_smth(&self) -> u32 {}
}
))
.unwrap();
let sails_path = &sails_paths::sails_path_or_default(None);
let discovered_svcs = discover_service_handlers(&service_impl, sails_path)
.iter()
.map(|fn_builder| fn_builder.ident.to_string())
.collect::<Vec<_>>();
assert_eq!(
discovered_svcs,
&[
"export_public_method_returning_self",
"export_public_method_returning_smth",
"export_public_method_returning_type"
]
);
}
}