feat (macro-rs): create macro_rs::napi macro

This commit is contained in:
sup39 2024-04-12 04:25:36 +09:00 committed by naskya
parent 0491e11a9e
commit 8b6e300d4e
No known key found for this signature in database
GPG Key ID: 712D413B3A9FED5C
5 changed files with 259 additions and 1 deletions

11
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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 }

View File

@ -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"] }

View File

@ -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());
}
}