feat (macro-rs): create macro_rs::napi macro
This commit is contained in:
parent
0491e11a9e
commit
8b6e300d4e
|
@ -196,6 +196,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"cuid2",
|
||||
"jsonschema",
|
||||
"macro_rs",
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
|
@ -1211,6 +1212,16 @@ version = "0.4.21"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
|
||||
[[package]]
|
||||
name = "macro_rs"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.58",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
[workspace]
|
||||
members = ["packages/backend-rs"]
|
||||
members = ["packages/backend-rs", "packages/macro-rs"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
macro_rs = { path = "packages/macro-rs" }
|
||||
|
||||
napi = { version = "2.16.2", default-features = false }
|
||||
napi-derive = "2.16.2"
|
||||
napi-build = "2.1.2"
|
||||
|
@ -11,16 +13,20 @@ async-trait = "0.1.79"
|
|||
basen = "0.1.0"
|
||||
cfg-if = "1.0.0"
|
||||
chrono = "0.4.37"
|
||||
convert_case = "0.6.0"
|
||||
cuid2 = "0.1.2"
|
||||
jsonschema = "0.17.1"
|
||||
once_cell = "1.19.0"
|
||||
parse-display = "0.9.0"
|
||||
pretty_assertions = "1.4.0"
|
||||
proc-macro2 = "1.0.79"
|
||||
quote = "1.0.35"
|
||||
rand = "0.8.5"
|
||||
schemars = "0.8.16"
|
||||
sea-orm = "0.12.15"
|
||||
serde = "1.0.197"
|
||||
serde_json = "1.0.115"
|
||||
syn = "2.0.58"
|
||||
thiserror = "1.0.58"
|
||||
tokio = "1.37.0"
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ napi = ["dep:napi", "dep:napi-derive"]
|
|||
crate-type = ["cdylib", "lib"]
|
||||
|
||||
[dependencies]
|
||||
macro_rs = { workspace = true }
|
||||
|
||||
napi = { workspace = true, optional = true, default-features = false, features = ["napi9", "tokio_rt", "chrono_date", "serde-json"] }
|
||||
napi-derive = { workspace = true, optional = true }
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "macro_rs"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.74"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
convert_case = { workspace = true }
|
||||
proc-macro2 = { workspace = true }
|
||||
quote = { workspace = true }
|
||||
syn = { workspace = true, features = ["full", "extra-traits"] }
|
|
@ -0,0 +1,225 @@
|
|||
use convert_case::{Case, Casing};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{quote, ToTokens};
|
||||
|
||||
/// Creates extra wrapper function for napi.
|
||||
///
|
||||
/// The types of the function arguments is converted with following rules:
|
||||
/// - `&str` and `&mut str` are converted to `String`
|
||||
/// - `&T` and `&mut T` are converted to `T`
|
||||
/// - Other `T` remains `T`
|
||||
///
|
||||
/// # Examples
|
||||
/// ## Example with `i32` argument
|
||||
/// ```rust
|
||||
/// #[macro_rs::napi]
|
||||
/// fn add_one(x: i32) -> i32 {
|
||||
/// x + 1
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// becomes
|
||||
///
|
||||
/// ```rust
|
||||
/// fn add_one(x: i32) -> i32 {
|
||||
/// x + 1
|
||||
/// }
|
||||
/// #[cfg_attr(feature = "napi", napi_derive::napi(js_name = "addOne"))]
|
||||
/// fn add_one_napi(x: i32) -> i32 {
|
||||
/// add_one(x)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Example with `&str` argument
|
||||
/// ```rust
|
||||
/// #[macro_rs::napi]
|
||||
/// fn concatenate_string(str1: &str, str2: &str) -> String {
|
||||
/// str1.to_owned() + str2
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// becomes
|
||||
///
|
||||
/// ```rust
|
||||
/// fn concatenate_string(str1: &str, str2: &str) -> String {
|
||||
/// str1.to_owned() + str2
|
||||
/// }
|
||||
/// #[cfg_attr(feature = "napi", napi_derive::napi(js_name = "concatenateString"))]
|
||||
/// fn concatenate_string_napi(str1: String, str2: String) -> String {
|
||||
/// concatenate_string(&str1, &str2)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// TODO: macro attributes are ignored
|
||||
#[proc_macro_attribute]
|
||||
pub fn napi(
|
||||
attr: proc_macro::TokenStream,
|
||||
item: proc_macro::TokenStream,
|
||||
) -> proc_macro::TokenStream {
|
||||
napi_impl(attr.into(), item.into()).into()
|
||||
}
|
||||
fn napi_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let item: syn::Item = syn::parse2(item).expect("Failed to parse TokenStream to syn::Item");
|
||||
// handle functions only
|
||||
let syn::Item::Fn(item_fn) = item else {
|
||||
// fallback to use napi_derive
|
||||
return quote! {
|
||||
#[napi_derive::napi(#attr)]
|
||||
#item
|
||||
};
|
||||
};
|
||||
|
||||
let ident = &item_fn.sig.ident;
|
||||
let js_name = ident.to_string().to_case(Case::Camel);
|
||||
|
||||
let item_fn_attrs = &item_fn.attrs;
|
||||
let item_fn_vis = &item_fn.vis;
|
||||
let mut item_fn_sig = item_fn.sig.clone();
|
||||
|
||||
// append "_napi" to function name
|
||||
item_fn_sig.ident = syn::parse_str(&format!("{}_napi", &ident)).unwrap();
|
||||
|
||||
// arguments in function call
|
||||
let called_args: Vec<TokenStream> = item_fn_sig
|
||||
.inputs
|
||||
.iter_mut()
|
||||
.map(|input| match input {
|
||||
// self
|
||||
syn::FnArg::Receiver(arg) => {
|
||||
let mut tokens = TokenStream::new();
|
||||
if let Some((ampersand, lifetime)) = &arg.reference {
|
||||
ampersand.to_tokens(&mut tokens);
|
||||
lifetime.to_tokens(&mut tokens);
|
||||
}
|
||||
arg.mutability.to_tokens(&mut tokens);
|
||||
arg.self_token.to_tokens(&mut tokens);
|
||||
tokens
|
||||
}
|
||||
// typed argument
|
||||
syn::FnArg::Typed(arg) => {
|
||||
match &mut *arg.pat {
|
||||
syn::Pat::Ident(ident) => {
|
||||
let name = &ident.ident;
|
||||
match &*arg.ty {
|
||||
// reference type argument => move ref from sigature to function call
|
||||
syn::Type::Reference(r) => {
|
||||
// add reference anotations to arguments in function call
|
||||
let mut tokens = TokenStream::new();
|
||||
r.and_token.to_tokens(&mut tokens);
|
||||
if let Some(lifetime) = &r.lifetime {
|
||||
lifetime.to_tokens(&mut tokens);
|
||||
}
|
||||
r.mutability.to_tokens(&mut tokens);
|
||||
name.to_tokens(&mut tokens);
|
||||
|
||||
// modify napi argument types in function sigature
|
||||
// (1) add `mut` token to `&mut` type
|
||||
ident.mutability = r.mutability;
|
||||
// (2) remove reference
|
||||
let elem_tokens = r.elem.to_token_stream();
|
||||
*arg.ty =
|
||||
syn::Type::Verbatim(match elem_tokens.to_string().as_str() {
|
||||
// &str => String
|
||||
"str" => quote! { String },
|
||||
// &T => T
|
||||
_ => elem_tokens,
|
||||
});
|
||||
|
||||
// return arguments in function call
|
||||
tokens
|
||||
}
|
||||
// o.w., return it as is
|
||||
_ => quote! { #name },
|
||||
}
|
||||
}
|
||||
pat => panic!("Unexpected FnArg: {pat:#?}"),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// TODO handle macro attr
|
||||
quote! {
|
||||
#item_fn
|
||||
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi(js_name = #js_name))]
|
||||
#(#item_fn_attrs)*
|
||||
#item_fn_vis #item_fn_sig {
|
||||
#ident(#(#called_args),*)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
|
||||
#[test]
|
||||
fn primitive_argument() {
|
||||
let generated = super::napi_impl(
|
||||
TokenStream::new(),
|
||||
quote! {
|
||||
fn add_one(x: i32) -> i32 {
|
||||
x + 1
|
||||
}
|
||||
},
|
||||
);
|
||||
let expected = quote! {
|
||||
fn add_one(x: i32) -> i32 {
|
||||
x + 1
|
||||
}
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi(js_name = "addOne"))]
|
||||
fn add_one_napi(x: i32) -> i32 {
|
||||
add_one(x)
|
||||
}
|
||||
};
|
||||
assert_eq!(generated.to_string(), expected.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn str_ref_argument() {
|
||||
let generated = super::napi_impl(
|
||||
TokenStream::new(),
|
||||
quote! {
|
||||
fn concatenate_string(str1: &str, str2: &str) -> String {
|
||||
str1.to_owned() + str2
|
||||
}
|
||||
},
|
||||
);
|
||||
let expected = quote! {
|
||||
fn concatenate_string(str1: &str, str2: &str) -> String {
|
||||
str1.to_owned() + str2
|
||||
}
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi(js_name = "concatenateString"))]
|
||||
fn concatenate_string_napi(str1: String, str2: String) -> String {
|
||||
concatenate_string(&str1, &str2)
|
||||
}
|
||||
};
|
||||
assert_eq!(generated.to_string(), expected.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mut_ref_argument() {
|
||||
let generated = super::napi_impl(
|
||||
TokenStream::new(),
|
||||
quote! {
|
||||
fn append_string_and_clone(base_str: &mut String, appended_str: &str) -> String {
|
||||
base_str.push_str(appended_str);
|
||||
base_str.to_owned()
|
||||
}
|
||||
},
|
||||
);
|
||||
let expected = quote! {
|
||||
fn append_string_and_clone(base_str: &mut String, appended_str: &str) -> String {
|
||||
base_str.push_str(appended_str);
|
||||
base_str.to_owned()
|
||||
}
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi(js_name = "appendStringAndClone"))]
|
||||
fn append_string_and_clone_napi(mut base_str: String, appended_str: String) -> String {
|
||||
append_string_and_clone(&mut base_str, &appended_str)
|
||||
}
|
||||
};
|
||||
assert_eq!(generated.to_string(), expected.to_string());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue