feat (macro-rs): convert Result<T, E> return type to napi::Result<T>

This commit is contained in:
sup39 2024-04-12 04:28:51 +09:00 committed by naskya
parent 5d2238a266
commit c231bf43cb
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
3 changed files with 145 additions and 10 deletions

2
Cargo.lock generated
View file

@ -1217,9 +1217,11 @@ name = "macro_rs"
version = "0.0.0"
dependencies = [
"convert_case",
"napi",
"proc-macro2",
"quote",
"syn 2.0.58",
"thiserror",
]
[[package]]

View file

@ -12,3 +12,8 @@ convert_case = { workspace = true }
proc-macro2 = { workspace = true }
quote = { workspace = true }
syn = { workspace = true, features = ["full", "extra-traits"] }
[dev-dependencies]
thiserror = { workspace = true }
napi = { workspace = true }
#napi-derive = { workspace = true }

View file

@ -2,6 +2,16 @@ use convert_case::{Case, Casing};
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
// FIXME
/// For doctest only
#[proc_macro_attribute]
pub fn dummy_macro(
_attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
item
}
/// Creates extra wrapper function for napi.
///
/// The types of the function arguments is converted with following rules:
@ -9,9 +19,13 @@ use quote::{quote, ToTokens};
/// - `&T` and `&mut T` are converted to `T`
/// - Other `T` remains `T`
///
/// In addition, return type `Result<T>` and `Result<T, E>` are converted to `napi::Result<T>`.
/// Note that `E` must implement `std::string::ToString` trait.
///
/// # Examples
/// ## Example with `i32` argument
/// ```rust
/// ```
/// # mod napi_derive { pub use macro_rs::dummy_macro as napi; } // FIXME
/// #[macro_rs::napi]
/// pub fn add_one(x: i32) -> i32 {
/// x + 1
@ -20,8 +34,11 @@ use quote::{quote, ToTokens};
///
/// generates
///
/// ```rust
/// #[cfg(feature = "napi")]
/// ```
/// # mod napi_derive { pub use macro_rs::dummy_macro as napi; } // FIXME
/// # pub fn add_one(x: i32) -> i32 {
/// # x + 1
/// # }
/// #[napi_derive::napi(js_name = "addOne")]
/// pub fn add_one_napi(x: i32) -> i32 {
/// add_one(x)
@ -29,7 +46,8 @@ use quote::{quote, ToTokens};
/// ```
///
/// ## Example with `&str` argument
/// ```rust
/// ```
/// # mod napi_derive { pub use macro_rs::dummy_macro as napi; } // FIXME
/// #[macro_rs::napi]
/// pub fn concatenate_string(str1: &str, str2: &str) -> String {
/// str1.to_owned() + str2
@ -38,14 +56,66 @@ use quote::{quote, ToTokens};
///
/// generates
///
/// ```rust
/// #[cfg(feature = "napi")]
/// ```
/// # mod napi_derive { pub use macro_rs::dummy_macro as napi; } // FIXME
/// # pub fn concatenate_string(str1: &str, str2: &str) -> String {
/// # str1.to_owned() + str2
/// # }
/// #[napi_derive::napi(js_name = "concatenateString")]
/// pub fn concatenate_string_napi(str1: String, str2: String) -> String {
/// concatenate_string(&str1, &str2)
/// }
/// ```
///
/// ## Example with `Result<T, E>` return type
/// ```
/// # mod napi_derive { pub use macro_rs::dummy_macro as napi; } // FIXME
/// #[derive(thiserror::Error, Debug)]
/// pub enum IntegerDivisionError {
/// #[error("Divided by zero")]
/// DividedByZero,
/// #[error("Not divisible with remainder = {0}")]
/// NotDivisible(i64),
/// }
///
/// #[macro_rs::napi]
/// pub fn integer_divide(dividend: i64, divisor: i64) -> Result<i64, IntegerDivisionError> {
/// match divisor {
/// 0 => Err(IntegerDivisionError::DividedByZero),
/// _ => match dividend % divisor {
/// 0 => Ok(dividend / divisor),
/// remainder => Err(IntegerDivisionError::NotDivisible(remainder)),
/// },
/// }
/// }
/// ```
///
/// generates
///
/// ```
/// # mod napi_derive { pub use macro_rs::dummy_macro as napi; } // FIXME
/// # #[derive(thiserror::Error, Debug)]
/// # pub enum IntegerDivisionError {
/// # #[error("Divided by zero")]
/// # DividedByZero,
/// # #[error("Not divisible with remainder = {0}")]
/// # NotDivisible(i64),
/// # }
/// # pub fn integer_divide(dividend: i64, divisor: i64) -> Result<i64, IntegerDivisionError> {
/// # match divisor {
/// # 0 => Err(IntegerDivisionError::DividedByZero),
/// # _ => match dividend % divisor {
/// # 0 => Ok(dividend / divisor),
/// # remainder => Err(IntegerDivisionError::NotDivisible(remainder)),
/// # },
/// # }
/// # }
/// #[napi_derive::napi(js_name = "integerDivide")]
/// pub fn integer_divide_napi(dividend: i64, divisor: i64) -> napi::Result<i64> {
/// integer_divide(dividend, divisor).map_err(|err| napi::Error::from_reason(err.to_string()))
/// }
/// ```
///
/// TODO: macro attributes are ignored
#[proc_macro_attribute]
pub fn napi(
@ -71,10 +141,41 @@ fn napi_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
let item_fn_attrs = &item_fn.attrs;
let item_fn_vis = &item_fn.vis;
let mut item_fn_sig = item_fn.sig.clone();
let mut function_call_modifiers = Vec::<TokenStream>::new();
// append "_napi" to function name
item_fn_sig.ident = syn::parse_str(&format!("{}_napi", &ident)).unwrap();
// convert return type `...::Result<T, ...>` to `napi::Result<T>`
if let syn::ReturnType::Type(_, ref mut return_type) = item_fn_sig.output {
if let Some(result_generic_type) = (|| {
let syn::Type::Path(return_type_path) = &**return_type else {
return None;
};
// match a::b::c::Result
let last_segment = return_type_path.path.segments.last()?;
if last_segment.ident != "Result" {
return None;
};
// extract <T, ...> from Result<T, ...>
let syn::PathArguments::AngleBracketed(generic_arguments) = &last_segment.arguments
else {
return None;
};
// return T only
generic_arguments.args.first()
})() {
// modify return type
*return_type = syn::parse_quote! {
napi::Result<#result_generic_type>
};
// add modifier to function call result
function_call_modifiers.push(quote! {
.map_err(|err| napi::Error::from_reason(err.to_string()))
});
}
};
// arguments in function call
let called_args: Vec<TokenStream> = item_fn_sig
.inputs
@ -138,11 +239,11 @@ fn napi_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
quote! {
#item_fn
#[cfg(feature = "napi")]
#[napi_derive::napi(js_name = #js_name)]
#(#item_fn_attrs)*
#item_fn_vis #item_fn_sig {
#ident(#(#called_args),*)
#(#function_call_modifiers)*
}
}
}
@ -170,7 +271,6 @@ mod tests {
}
},
quote! {
#[cfg(feature = "napi")]
#[napi_derive::napi(js_name = "addOne")]
pub fn add_one_napi(x: i32) -> i32 {
add_one(x)
@ -188,7 +288,6 @@ mod tests {
}
},
quote! {
#[cfg(feature = "napi")]
#[napi_derive::napi(js_name = "concatenateString")]
pub fn concatenate_string_napi(str1: String, str2: String) -> String {
concatenate_string(&str1, &str2)
@ -210,7 +309,6 @@ mod tests {
}
},
quote! {
#[cfg(feature = "napi")]
#[napi_derive::napi(js_name = "appendStringAndClone")]
pub fn append_string_and_clone_napi(
mut base_str: String,
@ -221,4 +319,34 @@ mod tests {
}
);
}
#[test]
fn result_return_type() {
test_macro!(
quote! {
pub fn integer_divide(
dividend: i64,
divisor: i64,
) -> Result<i64, IntegerDivisionError> {
match divisor {
0 => Err(IntegerDivisionError::DividedByZero),
_ => match dividend % divisor {
0 => Ok(dividend / divisor),
remainder => Err(IntegerDivisionError::NotDivisible(remainder)),
},
}
}
},
quote! {
#[napi_derive::napi(js_name = "integerDivide")]
pub fn integer_divide_napi(
dividend: i64,
divisor: i64,
) -> napi::Result<i64> {
integer_divide(dividend, divisor)
.map_err(|err| napi::Error::from_reason(err.to_string()))
}
}
);
}
}