Skip to content

Commit

Permalink
feat(macros): new export macro with unwrap result for fn impl (#728)
Browse files Browse the repository at this point in the history
Co-authored-by: Dennis Diatlov <dennis@gear-tech.io>
  • Loading branch information
vobradovich and DennisInSky authored Dec 13, 2024
1 parent 3bcf4f1 commit e60ec6a
Show file tree
Hide file tree
Showing 33 changed files with 1,133 additions and 50 deletions.
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ struct MyProgram;

#[program]
impl MyProgram {
#[route("ping")]
#[export(route = "ping")]
pub fn ping_svc(&self) -> MyPing {
MyPing::new()
}
Expand Down Expand Up @@ -96,6 +96,11 @@ to a caller.
> the application's balance to the caller's one. This can be done via using a dedicated
> type, `CommandReply<T>`.
Sometimes it is convenient to have a method that returns the `Result<T, E>` type,
but not expose it to clients. This allows using the `?` operator
in the method body. For this purpose, you can use the `#[export]` attribute macro with
the `unwrap_result` parameter.

```rust
#[service]
impl MyService {
Expand All @@ -109,6 +114,13 @@ impl MyService {
CommandReply::new(()).with_value(amount)
}

// This is a command returning `()` or panicking
#[export(unwrap_result)]
pub fn do_somethig_with_unwrap_result(&mut self, amount: u64) -> Result<(), String> {
do_somethig_returning_result()?;
Ok(())
}

// This is a query
pub fn something(&self, p1: Option<bool>) -> String {
...
Expand Down Expand Up @@ -165,7 +177,7 @@ impl MyProgram {


And the final key concept is message *__routing__*. This concept doesn't have a
mandatory representation in code, but can be altered by using the `#[route]`
mandatory representation in code, but can be altered by using the `#[export]`
attribute applied to those public methods and associated functions described above.
The concept itself is about rules for dispatching an incoming request message to
a specific service's method using service and method names. By default, every
Expand All @@ -182,13 +194,13 @@ impl MyProgram {
}
```

This behavior can be changed by applying the `#[route]` attribute:
This behavior can be changed by applying the `#[export]` attribute with `route` parameter:

```rust
#[program]
impl MyProgram {
// The `MyPing` service is exposed as `Ping`
#[route("ping")] // The specified name will be converted into PascalCase
#[export(route = "ping")] // The specified name will be converted into PascalCase
pub fn ping_svc(&self) -> MyPing {
...
}
Expand All @@ -201,7 +213,7 @@ The same rules are applicable to service method names:
#[service]
impl MyPing {
// The `do_ping` method is exposed as `Ping`
#[route("ping")]
#[export(route = "ping")]
pub fn do_ping(&mut self) {
...
}
Expand Down Expand Up @@ -504,7 +516,7 @@ Here is a brief overview of features mentioned above and showcased by the exampl

The examples are composed on a principle of a few programs exposing several services.
See [DemoProgram](/examples/demo/app/src/lib.rs) which demonstrates this, including
the use of program's multiple constructors and the `#[route]` attribute for one of
the use of program's multiple constructors and the `#[export]` attribute for one of
the exposed services. The example also includes Rust [build script](/examples/demo/app/build.rs)
building the program as a WASM app ready for loading onto Gear network.

Expand Down
13 changes: 7 additions & 6 deletions examples/demo/app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,24 @@ impl DemoProgram {
}

/// Another program constructor (called once at the very beginning of the program lifetime)
pub fn new(counter: Option<u32>, dog_position: Option<(i32, i32)>) -> Self {
#[export(unwrap_result)]
pub fn new(counter: Option<u32>, dog_position: Option<(i32, i32)>) -> Result<Self, String> {
unsafe {
let dog_position = dog_position.unwrap_or_default();
DOG_DATA = Some(RefCell::new(walker::WalkerData::new(
dog_position.0,
dog_position.1,
)));
}
Self {
Ok(Self {
counter_data: RefCell::new(counter::CounterData::new(counter.unwrap_or_default())),
}
})
}

// Exposing service with overriden route
#[route("ping_pong")]
pub fn ping(&self) -> ping::PingService {
ping::PingService::default()
#[export(route = "ping_pong", unwrap_result)]
pub fn ping(&self) -> Result<ping::PingService, String> {
Ok(ping::PingService::default())
}

// Exposing another service
Expand Down
2 changes: 1 addition & 1 deletion examples/rmrk/catalog/app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ impl Program {
}

// Expose hosted service
#[sails_rs::route("RmrkCatalog")]
#[sails_rs::export(route = "RmrkCatalog")]
pub fn catalog(&self) -> Catalog<GStdExecContext> {
let exec_context = GStdExecContext::default();
Catalog::new(exec_context)
Expand Down
4 changes: 2 additions & 2 deletions examples/rmrk/resource/app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#[cfg(not(target_arch = "wasm32"))]
pub extern crate std;

use sails_rs::gstd::{calls::GStdRemoting, program, route, GStdExecContext};
use sails_rs::gstd::{calls::GStdRemoting, program, GStdExecContext};
use services::ResourceStorage;

mod catalogs;
Expand All @@ -27,7 +27,7 @@ impl Program {
}

// Expose hosted service
#[route("RmrkResource")]
#[export(route = "RmrkResource")]
pub fn resource_storage(&self) -> ResourceStorage<GStdExecContext, RmrkCatalog> {
ResourceStorage::new(GStdExecContext::default(), RmrkCatalog::new(GStdRemoting))
}
Expand Down
128 changes: 128 additions & 0 deletions rs/macros/core/src/export/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use proc_macro_error::abort;
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
LitBool, LitStr, Path, Token,
};

#[derive(PartialEq, Debug, Default)]
pub(crate) struct ExportArgs {
route: Option<String>,
unwrap_result: bool,
}

impl ExportArgs {
pub fn route(&self) -> Option<&str> {
self.route.as_deref()
}

pub fn unwrap_result(&self) -> bool {
self.unwrap_result
}
}

impl Parse for ExportArgs {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let punctuated: Punctuated<ImportArg, Token![,]> = Punctuated::parse_terminated(input)?;
let mut args = Self {
route: None,
unwrap_result: false,
};
for arg in punctuated {
match arg {
ImportArg::Route(route) => {
args.route = Some(route);
}
ImportArg::UnwrapResult(unwrap_result) => {
args.unwrap_result = unwrap_result;
}
}
}
Ok(args)
}
}

#[derive(Debug)]
enum ImportArg {
Route(String),
UnwrapResult(bool),
}

impl Parse for ImportArg {
fn parse(input: ParseStream) -> syn::Result<Self> {
let path = input.parse::<Path>()?;
let ident = path.get_ident().unwrap();
match ident.to_string().as_str() {
"route" => {
input.parse::<Token![=]>()?;
if let Ok(route) = input.parse::<LitStr>() {
return Ok(Self::Route(route.value()));
}
abort!(ident, "unexpected value for `route` argument: {}", input)
}
"unwrap_result" => {
if input.parse::<Token![=]>().is_ok() {
if let Ok(val) = input.parse::<LitBool>() {
return Ok(Self::UnwrapResult(val.value()));
}
}
Ok(Self::UnwrapResult(true))
}
_ => abort!(ident, "unknown argument: {}", ident),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use quote::quote;

#[test]
fn export_parse_args() {
// arrange
let input = quote!(route = "CallMe", unwrap_result);
let expected = ExportArgs {
route: Some("CallMe".to_owned()),
unwrap_result: true,
};

// act
let args = syn::parse2::<ExportArgs>(input).unwrap();

// arrange
assert_eq!(expected, args);
}

#[test]
fn export_parse_args_unwrap_result() {
// arrange
let input = quote!(unwrap_result);
let expected = ExportArgs {
route: None,
unwrap_result: true,
};

// act
let args = syn::parse2::<ExportArgs>(input).unwrap();

// arrange
assert_eq!(expected, args);
}

#[test]
fn export_parse_args_unwrap_result_eq_false() {
// arrange
let input = quote!(unwrap_result = false);
let expected = ExportArgs {
route: None,
unwrap_result: false,
};

// act
let args = syn::parse2::<ExportArgs>(input).unwrap();

// arrange
assert_eq!(expected, args);
}
}
104 changes: 104 additions & 0 deletions rs/macros/core/src/export/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use args::ExportArgs;
use convert_case::{Case, Casing};
use proc_macro2::{Span, TokenStream};
use proc_macro_error::abort;
use syn::{parse::Parse, spanned::Spanned, Attribute, ImplItemFn};

use crate::{route, shared};

mod args;

pub fn export(attrs: TokenStream, impl_item_fn_tokens: TokenStream) -> TokenStream {
let fn_impl: ImplItemFn = syn::parse2::<ImplItemFn>(impl_item_fn_tokens.clone())
.unwrap_or_else(|err| {
abort!(
err.span(),
"`export` attribute can be applied to methods only: {}",
err
)
});
ensure_pub_visibility(&fn_impl);
ensure_single_export_or_route_on_impl(&fn_impl);
let args = syn::parse2::<ExportArgs>(attrs)
.unwrap_or_else(|_| abort!(fn_impl.span(), "`export` attribute cannot be parsed"));
ensure_returns_result_with_unwrap_result(fn_impl, args);
impl_item_fn_tokens
}

fn ensure_pub_visibility(fn_impl: &ImplItemFn) {
match fn_impl.vis {
syn::Visibility::Public(_) => (),
_ => abort!(
fn_impl.span(),
"`export` attribute can be applied to public methods only"
),
}
}

pub(crate) fn ensure_single_export_or_route_on_impl(fn_impl: &ImplItemFn) {
let attr_export = fn_impl.attrs.iter().find(|attr| {
attr.meta
.path()
.segments
.last()
.map(|s| s.ident == "export" || s.ident == "route")
.unwrap_or(false)
});
if attr_export.is_some() {
abort!(
fn_impl,
"multiple `export` or `route` attributes on the same method are not allowed",
)
}
}

fn ensure_returns_result_with_unwrap_result(fn_impl: ImplItemFn, args: ExportArgs) {
// ensure Result type is returned if unwrap_result is set to true
_ = shared::unwrap_result_type(&fn_impl.sig, args.unwrap_result());
}

pub(crate) fn invocation_export(fn_impl: &ImplItemFn) -> (Span, String, bool) {
if let Some((args, span)) = parse_export_args(&fn_impl.attrs) {
let ident = &fn_impl.sig.ident;
let unwrap_result = args.unwrap_result();
args.route().map_or_else(
|| {
(
ident.span(),
ident.to_string().to_case(Case::Pascal),
unwrap_result,
)
},
|route| (span, route.to_case(Case::Pascal), unwrap_result),
)
} else {
let (span, route) = route::invocation_route(fn_impl);
(span, route, false)
}
}

fn parse_export_args(attrs: &[Attribute]) -> Option<(ExportArgs, Span)> {
attrs
.iter()
.filter_map(|attr| parse_attr(attr).map(|args| (args, attr.meta.span())))
.next()
}

pub(crate) fn parse_attr(attr: &Attribute) -> Option<ExportArgs> {
let meta = attr.meta.require_list().ok()?;
if meta
.path
.segments
.last()
.is_some_and(|s| s.ident == "export")
{
let args = meta
.parse_args_with(ExportArgs::parse)
.unwrap_or_else(|er| {
abort!(meta.span(), "`export` attribute cannot be parsed: {}", er)
});
Some(args)
} else {
None
}
}
2 changes: 2 additions & 0 deletions rs/macros/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@

//! Implemntation of the procedural macros exposed via the `sails-macros` crate.
pub use export::export;
pub use program::__gprogram_internal;
pub use program::gprogram;
pub use route::groute;
pub use service::__gservice_internal;
pub use service::gservice;

mod export;
mod program;
mod route;
mod sails_paths;
Expand Down
Loading

0 comments on commit e60ec6a

Please sign in to comment.