-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(macros): new
export
macro with unwrap result for fn impl (#728)
Co-authored-by: Dennis Diatlov <dennis@gear-tech.io>
- Loading branch information
1 parent
3bcf4f1
commit e60ec6a
Showing
33 changed files
with
1,133 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.