From 0cb4fbf6b4119de0b003e8c08ad16717cc2188df Mon Sep 17 00:00:00 2001 From: sup39 Date: Wed, 12 Jun 2024 00:49:22 +0800 Subject: [PATCH] refactor: macro-rs -> macros + macros_impl Co-authored-by: naskya --- .gitlab-ci.yml | 2 +- Cargo.lock | 18 +- Cargo.toml | 5 +- Dockerfile | 4 +- packages/backend-rs/Cargo.toml | 2 +- packages/backend-rs/src/config/server.rs | 2 +- packages/backend-rs/src/lib.rs | 2 +- packages/macro-rs/macros-impl/Cargo.toml | 11 + packages/macro-rs/macros-impl/src/lib.rs | 2 + packages/macro-rs/macros-impl/src/napi.rs | 451 ++++++++++++ packages/macro-rs/macros-impl/src/util/mod.rs | 3 + .../macro-rs/macros-impl/src/util/tester.rs | 121 ++++ packages/macro-rs/{ => macros}/Cargo.toml | 11 +- packages/macro-rs/macros/src/helper.rs | 88 +++ packages/macro-rs/macros/src/lib.rs | 81 +++ packages/macro-rs/src/lib.rs | 680 ------------------ 16 files changed, 780 insertions(+), 703 deletions(-) create mode 100644 packages/macro-rs/macros-impl/Cargo.toml create mode 100644 packages/macro-rs/macros-impl/src/lib.rs create mode 100644 packages/macro-rs/macros-impl/src/napi.rs create mode 100644 packages/macro-rs/macros-impl/src/util/mod.rs create mode 100644 packages/macro-rs/macros-impl/src/util/tester.rs rename packages/macro-rs/{ => macros}/Cargo.toml (52%) create mode 100644 packages/macro-rs/macros/src/helper.rs create mode 100644 packages/macro-rs/macros/src/lib.rs delete mode 100644 packages/macro-rs/src/lib.rs diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d68ae43810..7b12b3d7a5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -290,7 +290,7 @@ cargo:doc: - cp ci/cargo/config.toml /usr/local/cargo/config.toml script: - cargo doc --document-private-items - - printf "window.ALL_CRATES = ['backend_rs', 'macro_rs'];" > target/doc/crates.js + - printf 'window.ALL_CRATES = ["backend_rs", "macros", "macros_impl"];' > target/doc/crates.js - printf '' 'backend_rs' > target/doc/index.html - cd target/doc - npx --yes netlify-cli deploy --prod --site="${CARGO_DOC_SITE_ID}" --dir=. diff --git a/Cargo.lock b/Cargo.lock index 75db34e287..37ac78a5d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -210,7 +210,7 @@ dependencies = [ "idna", "image", "isahc", - "macro-rs", + "macros", "napi", "napi-build", "napi-derive", @@ -1718,18 +1718,24 @@ dependencies = [ ] [[package]] -name = "macro-rs" +name = "macros" version = "0.0.0" dependencies = [ - "convert_case", - "napi", - "napi-derive", + "macros-impl", "proc-macro2", "quote", "serde", "serde_json", +] + +[[package]] +name = "macros-impl" +version = "0.0.0" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", "syn 2.0.66", - "thiserror", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0e3536a5b3..1fe4e70f51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,10 @@ [workspace] -members = ["packages/backend-rs", "packages/macro-rs"] +members = ["packages/backend-rs", "packages/macro-rs/macros", "packages/macro-rs/macros-impl"] resolver = "2" [workspace.dependencies] -macro-rs = { path = "packages/macro-rs" } +macros = { path = "packages/macro-rs/macros" } +macros-impl = { path = "packages/macro-rs/macros-impl" } napi = { git = "https://github.com/napi-rs/napi-rs.git", rev = "ca2cd5c35a0c39ec4a94e93c6c5695b681046df2" } napi-derive = "2.16.5" diff --git a/Dockerfile b/Dockerfile index 5bad58b949..789f242070 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,8 +17,7 @@ COPY Cargo.toml Cargo.toml COPY Cargo.lock Cargo.lock COPY packages/backend-rs/Cargo.toml packages/backend-rs/Cargo.toml COPY packages/backend-rs/src/lib.rs packages/backend-rs/src/ -COPY packages/macro-rs/Cargo.toml packages/macro-rs/Cargo.toml -COPY packages/macro-rs/src/lib.rs packages/macro-rs/src/ +COPY packages/macro-rs packages/macro-rs/ # Configure pnpm, and install backend-rs dependencies RUN corepack enable && corepack prepare pnpm@latest --activate && pnpm --filter backend-rs install @@ -26,7 +25,6 @@ RUN cargo fetch --locked --manifest-path Cargo.toml # Copy in the rest of the rust files COPY packages/backend-rs packages/backend-rs/ -# COPY packages/macro-rs packages/macro-rs/ # Compile backend-rs RUN NODE_ENV='production' pnpm run --filter backend-rs build diff --git a/packages/backend-rs/Cargo.toml b/packages/backend-rs/Cargo.toml index 996a95bce4..790ad0f317 100644 --- a/packages/backend-rs/Cargo.toml +++ b/packages/backend-rs/Cargo.toml @@ -12,7 +12,7 @@ napi = ["dep:napi", "dep:napi-derive", "dep:napi-build"] crate-type = ["cdylib", "lib"] [dependencies] -macro-rs = { workspace = true } +macros = { workspace = true } napi = { workspace = true, optional = true, features = ["chrono_date", "napi4", "serde-json", "tokio_rt"] } napi-derive = { workspace = true, optional = true } diff --git a/packages/backend-rs/src/config/server.rs b/packages/backend-rs/src/config/server.rs index 5170b0617e..8991caec99 100644 --- a/packages/backend-rs/src/config/server.rs +++ b/packages/backend-rs/src/config/server.rs @@ -4,7 +4,7 @@ use once_cell::sync::Lazy; use serde::Deserialize; use std::{env, fs}; -pub const VERSION: &str = macro_rs::read_version_from_package_json!(); +pub const VERSION: &str = macros::read_version_from_package_json!(); #[derive(Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/packages/backend-rs/src/lib.rs b/packages/backend-rs/src/lib.rs index 6a124de340..dbb3f734a1 100644 --- a/packages/backend-rs/src/lib.rs +++ b/packages/backend-rs/src/lib.rs @@ -1,4 +1,4 @@ -use macro_rs::{derive_clone_and_export, export, ts_export}; +use macros::{derive_clone_and_export, export, ts_export}; pub mod config; pub mod database; diff --git a/packages/macro-rs/macros-impl/Cargo.toml b/packages/macro-rs/macros-impl/Cargo.toml new file mode 100644 index 0000000000..8029372236 --- /dev/null +++ b/packages/macro-rs/macros-impl/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "macros-impl" +version = "0.0.0" +edition = "2021" +rust-version = "1.74" + +[dependencies] +convert_case = { workspace = true } +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true, features = ["clone-impls", "extra-traits", "full", "parsing", "printing"] } diff --git a/packages/macro-rs/macros-impl/src/lib.rs b/packages/macro-rs/macros-impl/src/lib.rs new file mode 100644 index 0000000000..8310f17148 --- /dev/null +++ b/packages/macro-rs/macros-impl/src/lib.rs @@ -0,0 +1,2 @@ +pub mod napi; +mod util; diff --git a/packages/macro-rs/macros-impl/src/napi.rs b/packages/macro-rs/macros-impl/src/napi.rs new file mode 100644 index 0000000000..11d9283d5b --- /dev/null +++ b/packages/macro-rs/macros-impl/src/napi.rs @@ -0,0 +1,451 @@ +//! Napi related macros + +use convert_case::{Case, Casing}; +use proc_macro2::{TokenStream, TokenTree}; +use quote::{quote, ToTokens}; + +/// Creates an extra wrapper function for [napi_derive](https://docs.rs/napi-derive/latest/napi_derive/). +/// +/// The macro is simply converted into `napi_derive::napi(...)` +/// if it is not applied to a function. +/// +/// The macro sets the following attributes by default if not specified: +/// - `use_nullable = true` (if `object` or `constructor` attribute is specified) +/// - `js_name` to the camelCase version of the original function name (for functions) +/// +/// 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 [`Vec`] +/// - `&T` and `&mut T` are converted to `T` +/// - Other `T` remains `T` +/// +/// In addition, return type [`Result`] and [`Result`] are converted to [`napi::Result`](https://docs.rs/napi/latest/napi/type.Result.html). +/// Note that `E` must implement [std::error::Error] trait, +/// and `crate::util::error_chain::format_error(error: &dyn std::error::Error) -> String` function must be present. +/// +/// # Examples +/// ## Applying the macro to a struct +/// ``` +/// # use macros_impl::napi::napi; +/// # macros_impl::macro_doctest!({ +/// #[macros::napi(object)] +/// struct Person { +/// id: i32, +/// name: String, +/// } +/// +/// # }, { +/// /******* the code above expands to *******/ +/// +/// #[napi_derive::napi(use_nullable = true, object)] +/// struct Person { +/// id: i32, +/// name: String, +/// } +/// # }); +/// ``` +/// +/// ## Function with explicitly specified `js_name` +/// ``` +/// # use macros_impl::napi::napi; +/// # macros_impl::macro_doctest!({ +/// #[macros::napi(js_name = "add1")] +/// pub fn add_one(x: i32) -> i32 { +/// x + 1 +/// } +/// +/// # }, { +/// /******* the code above expands to *******/ +/// +/// pub fn add_one(x: i32) -> i32 { +/// x + 1 +/// } +/// +/// #[napi_derive::napi(js_name = "add1")] +/// pub fn add_one_napi(x: i32) -> i32 { +/// add_one(x) +/// } +/// # }); +/// ``` +/// +/// ## Function with `i32` argument +/// ``` +/// # use macros_impl::napi::napi; +/// # macros_impl::macro_doctest!({ +/// #[macros::napi] +/// pub fn add_one(x: i32) -> i32 { +/// x + 1 +/// } +/// +/// # }, { +/// /******* the code above expands to *******/ +/// +/// 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) +/// } +/// # }); +/// ``` +/// +/// ## Function with `&str` argument +/// ``` +/// # use macros_impl::napi::napi; +/// # macros_impl::macro_doctest!({ +/// #[macros::napi] +/// pub fn concatenate_string(str1: &str, str2: &str) -> String { +/// str1.to_owned() + str2 +/// } +/// +/// # }, { +/// /******* the code above expands to *******/ +/// +/// 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) +/// } +/// # }); +/// ``` +/// +/// ## Function with `&[String]` argument +/// ``` +/// # use macros_impl::napi::napi; +/// # macros_impl::macro_doctest!({ +/// #[macros::napi] +/// pub fn string_array_length(array: &[String]) -> u32 { +/// array.len() as u32 +/// } +/// +/// # }, { +/// /******* the code above expands to *******/ +/// +/// pub fn string_array_length(array: &[String]) -> u32 { +/// array.len() as u32 +/// } +/// +/// #[napi_derive::napi(js_name = "stringArrayLength",)] +/// pub fn string_array_length_napi(array: Vec) -> u32 { +/// string_array_length(&array) +/// } +/// # }); +/// ``` +/// +/// ## Function with `Result` return type +/// ``` +/// # quote::quote! { // prevent compiling the code +/// #[derive(thiserror::Error, Debug)] +/// pub enum IntegerDivisionError { +/// #[error("Divided by zero")] +/// DividedByZero, +/// #[error("Not divisible with remainder {0}")] +/// NotDivisible(i64), +/// } +/// # }; +/// +/// # use macros_impl::napi::napi; +/// # macros_impl::macro_doctest!({ +/// #[macros::napi] +/// pub fn integer_divide(dividend: i64, divisor: i64) -> Result { +/// match divisor { +/// 0 => Err(IntegerDivisionError::DividedByZero), +/// _ => match dividend % divisor { +/// 0 => Ok(dividend / divisor), +/// remainder => Err(IntegerDivisionError::NotDivisible(remainder)), +/// }, +/// } +/// } +/// # }, { +/// +/// /******* the function above expands to *******/ +/// +/// pub fn integer_divide(dividend: i64, divisor: i64) -> Result { +/// 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 { +/// integer_divide(dividend, divisor) +/// .map_err(|err| napi::Error::from_reason(crate::util::error_chain::format_error(&err))) +/// } +/// # }); +/// ``` +/// +pub fn napi(macro_attr: TokenStream, item: TokenStream) -> TokenStream { + let macro_attr_tokens: Vec = macro_attr.clone().into_iter().collect(); + // generated extra macro attr TokenStream (prepended before original input `macro_attr`) + let mut extra_macro_attr = TokenStream::new(); + + let item: syn::Item = + syn::parse2(item).expect("Failed to parse input TokenStream to syn::Item"); + + // handle non-functions + let syn::Item::Fn(item_fn) = item else { + // set `use_nullable = true` if `object` or `constructor` present but not `use_nullable` + if macro_attr_tokens.iter().any(|token| { + matches!(token, TokenTree::Ident(ident) if ident == "object" || ident == "constructor") + }) && !macro_attr_tokens.iter().any(|token| { + matches!(token, TokenTree::Ident(ident) if ident == "use_nullable") + }) { + quote! { use_nullable = true, }.to_tokens(&mut extra_macro_attr); + } + return quote! { + #[napi_derive::napi(#extra_macro_attr #macro_attr)] + #item + }; + }; + + // handle functions + let ident = &item_fn.sig.ident; + 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::::new(); + + // append "_napi" to function name + item_fn_sig.ident = syn::parse_str(&format!("{}_napi", &ident)).unwrap(); + + // append `.await` to function call in async function + if item_fn_sig.asyncness.is_some() { + function_call_modifiers.push(quote! { + .await + }); + } + + // convert return type `...::Result` to `napi::Result` + 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 from Result + 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(crate::util::error_chain::format_error(&err))) + }); + } + }; + + // arguments in function call + let called_args: Vec = 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 + *arg.ty = syn::Type::Verbatim(match &*r.elem { + syn::Type::Slice(slice) => { + let ty = &*slice.elem; + quote! { Vec<#ty> } + } + _ => { + let elem_tokens = r.elem.to_token_stream(); + 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(); + + // handle macro attr + // set js_name if not specified + if !macro_attr_tokens + .iter() + .any(|token| matches!(token, TokenTree::Ident(ident) if ident == "js_name")) + { + let js_name = ident.to_string().to_case(Case::Camel); + quote! { js_name = #js_name, }.to_tokens(&mut extra_macro_attr); + } + + quote! { + #item_fn + + #[napi_derive::napi(#extra_macro_attr #macro_attr)] + #(#item_fn_attrs)* + #item_fn_vis #item_fn_sig { + #ident(#(#called_args),*) + #(#function_call_modifiers)* + } + } +} + +crate::macro_unit_tests! { + mut_ref_argument: { + #[macros::napi] + pub fn append_string_and_clone( + base_str: &mut String, + appended_str: &str, + ) -> String { + base_str.push_str(appended_str); + base_str.to_owned() + } + } generates { + #[napi_derive::napi(js_name = "appendStringAndClone", )] + pub fn append_string_and_clone_napi( + mut base_str: String, + appended_str: String, + ) -> String { + append_string_and_clone(&mut base_str, &appended_str) + } + } + + result_return_type: { + #[macros::napi] + pub fn integer_divide( + dividend: i64, + divisor: i64, + ) -> Result { + match divisor { + 0 => Err(IntegerDivisionError::DividedByZero), + _ => match dividend % divisor { + 0 => Ok(dividend / divisor), + remainder => Err(IntegerDivisionError::NotDivisible(remainder)), + }, + } + } + } generates { + #[napi_derive::napi(js_name = "integerDivide", )] + pub fn integer_divide_napi( + dividend: i64, + divisor: i64, + ) -> napi::Result { + integer_divide(dividend, divisor) + .map_err(|err| napi::Error::from_reason(crate::util::error_chain::format_error(&err))) + } + } + + async_function: { + #[macros::napi] + pub async fn async_add_one(x: i32) -> i32 { + x + 1 + } + } generates { + #[napi_derive::napi(js_name = "asyncAddOne", )] + pub async fn async_add_one_napi(x: i32) -> i32 { + async_add_one(x) + .await + } + } + + slice_type: { + #[macros::napi] + pub fn string_array_length(array: &[String]) -> u32 { + array.len() as u32 + } + } generates { + #[napi_derive::napi(js_name = "stringArrayLength", )] + pub fn string_array_length_napi(array: Vec) -> u32 { + string_array_length(&array) + } + } + + object_with_explicitly_set_use_nullable: { + #[macros::napi(object, use_nullable = false)] + struct Person { + id: i32, + name: Option, + } + } becomes { + #[napi_derive::napi(object, use_nullable = false)] + struct Person { + id: i32, + name: Option, + } + } + + macro_attr: { + #[macros::napi(ts_return_type = "number")] + pub fn add_one(x: i32) -> i32 { + x + 1 + } + } generates { + #[napi_derive::napi(js_name = "addOne", ts_return_type = "number")] + pub fn add_one_napi(x: i32) -> i32 { + add_one(x) + } + } + + explicitly_specified_js_name_and_other_macro_attr: { + #[macros::napi(ts_return_type = "number", js_name = "add1")] + pub fn add_one(x: i32) -> i32 { + x + 1 + } + } generates { + #[napi_derive::napi(ts_return_type = "number", js_name = "add1")] + pub fn add_one_napi(x: i32) -> i32 { + add_one(x) + } + } +} diff --git a/packages/macro-rs/macros-impl/src/util/mod.rs b/packages/macro-rs/macros-impl/src/util/mod.rs new file mode 100644 index 0000000000..83be476077 --- /dev/null +++ b/packages/macro-rs/macros-impl/src/util/mod.rs @@ -0,0 +1,3 @@ +//! Utilities for developing procedural macros + +mod tester; diff --git a/packages/macro-rs/macros-impl/src/util/tester.rs b/packages/macro-rs/macros-impl/src/util/tester.rs new file mode 100644 index 0000000000..2762f3d4fc --- /dev/null +++ b/packages/macro-rs/macros-impl/src/util/tester.rs @@ -0,0 +1,121 @@ +//! Macros for testing procedural macros + +/// Tests if the macro expands correctly. +/// +/// # Examples +/// ``` +/// use macros_impl::napi::napi; +/// +/// macros_impl::macro_doctest!({ +/// #[macros::napi(object)] +/// struct Person { +/// id: i32, +/// name: String, +/// } +/// }, { +/// #[napi_derive::napi(use_nullable = true, object)] +/// struct Person { +/// id: i32, +/// name: String, +/// } +/// }); +/// ``` +#[macro_export] +macro_rules! macro_doctest { + ({ + #[macros :: $macro_name:ident $(( $($attr:tt)* ))?] + $($item:tt)* + }, { + $($expanded:tt)* + }) => { + assert_eq!( + ::std::string::ToString::to_string( + &$macro_name( + ::quote::quote!($( $($attr)* )?), + ::quote::quote!($($item)*), + ) + ), + ::std::string::ToString::to_string( + &::quote::quote!($($expanded)*) + ) + ); + }; +} + +/// Creates unit tests for macros. +/// +/// # Examples +/// ``` +/// macros_impl::macro_unit_tests! { +/// add1_becomes: { +/// #[macros::napi(js_name = "add1")] +/// pub fn add_one(x: i32) -> i32 { +/// x + 1 +/// } +/// } becomes { // the code above should expand to the following code +/// pub fn add_one(x: i32) -> i32 { +/// x + 1 +/// } +/// +/// #[napi_derive::napi(js_name = "add1")] +/// pub fn add_one_napi(x: i32) -> i32 { +/// add_one(x) +/// } +/// } +/// +/// // this test case is equivalent to `add1_becomes` +/// add1_generates: { +/// #[macros::napi(js_name = "add1")] +/// pub fn add_one(x: i32) -> i32 { +/// x + 1 +/// } +/// } generates { // the code above should generate the following code +/// #[napi_derive::napi(js_name = "add1")] +/// pub fn add_one_napi(x: i32) -> i32 { +/// add_one(x) +/// } +/// } +/// } +/// ``` +#[macro_export] macro_rules! macro_unit_tests { + (@test $macro_name:ident($attr:ident, $item:ident) becomes $expanded:ident) => { + assert_eq!( + ::std::format!("{}", $macro_name($attr, $item)), + ::std::format!("{}", $expanded), + ); + }; + (@test $macro_name:ident($attr:ident, $item:ident) generates $expanded:ident) => { + let item_str = format!("{}", $item); + assert_eq!( + ::std::format!("{}", $macro_name($attr, $item)), + ::std::format!("{} {}", item_str, $expanded), + ); + }; + + ( + $( + $test_name:ident : { + #[macros :: $macro_name:ident $(( $($attr:tt)* ))?] + $($item:tt)* + } $op:tt { + $($expanded:tt)* + } + )* + ) => { + #[cfg(test)] + mod unit_test { + use super::*; + + $( + #[test] + fn $test_name() { + let attr = ::quote::quote!($( $($attr)* )?); + let item = ::quote::quote!($($item)*); + let expanded = ::quote::quote!($($expanded)*); + + $crate::macro_unit_tests!(@test $macro_name(attr, item) $op expanded); + } + )* + } + }; +} diff --git a/packages/macro-rs/Cargo.toml b/packages/macro-rs/macros/Cargo.toml similarity index 52% rename from packages/macro-rs/Cargo.toml rename to packages/macro-rs/macros/Cargo.toml index c805fcce36..6b10d7ac1d 100644 --- a/packages/macro-rs/Cargo.toml +++ b/packages/macro-rs/macros/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "macro-rs" +name = "macros" version = "0.0.0" edition = "2021" rust-version = "1.74" @@ -8,14 +8,9 @@ rust-version = "1.74" proc-macro = true [dependencies] -convert_case = { workspace = true } +macros-impl = { workspace = true } + proc-macro2 = { workspace = true } quote = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = ["std"] } -syn = { workspace = true, features = ["extra-traits", "full"] } - -[dev-dependencies] -thiserror = { workspace = true } -napi = { workspace = true } -napi-derive = { workspace = true, features = ["noop"] } diff --git a/packages/macro-rs/macros/src/helper.rs b/packages/macro-rs/macros/src/helper.rs new file mode 100644 index 0000000000..a901ba27e9 --- /dev/null +++ b/packages/macro-rs/macros/src/helper.rs @@ -0,0 +1,88 @@ +//! Helper macros for developing procedural macros + +#[doc(hidden)] +pub(crate) use quote::quote; + +/// Defines wrapper #\[proc_macro_attribute]s. +/// +/// # Examples +/// ```ignore +/// define_wrapper_proc_macro_attributes! { +/// // expand `#[export(attr)]` to +/// // ``` +/// // #[cfg_attr(feature = "napi", macros::napi(#attr))] +/// // ``` +/// export(attr, item) { +/// #[cfg_attr(feature = "napi", macros::napi(#attr))] +/// #item +/// } +/// +/// // expand `#[ts_export(attr)]` to +/// // ``` +/// // #[cfg(feature = "napi")] +/// // #[macros::napi(#attr)] +/// // ``` +/// ts_export(attr, item) { +/// #[cfg(feature = "napi")] +/// #[macros::napi(#attr)] +/// #item +/// } +/// } +/// ``` +macro_rules! define_wrapper_proc_macro_attributes { + ( + $( + $(#[$meta:meta])* + $macro_name:ident ($arg_attr:ident, $arg_item:ident) { + $($body:tt)* + } + )* + ) => { + $( + $(#[$meta])* + #[proc_macro_attribute] + pub fn $macro_name( + attr: ::proc_macro::TokenStream, + item: ::proc_macro::TokenStream, + ) -> ::proc_macro::TokenStream { + let $arg_attr: ::proc_macro2::TokenStream = attr.into(); + let $arg_item: ::proc_macro2::TokenStream = item.into(); + ::quote::quote!($($body)*).into() + } + )* + } +} +pub(crate) use define_wrapper_proc_macro_attributes; + +/// Wraps and exports #\[proc_macro_attribute] implementation. +/// +/// # Examples +/// ```ignore +/// reexport_proc_macro_attributes! { +/// // wrap and export [macros_impl::napi::napi] as #[macros::napi] +/// macros_impl::napi::napi as napi +/// +/// // wrap and export [macros_impl::errors::errors] as #[macros::errors] +/// macros_impl::errors::errors as errors +/// } +/// ``` +macro_rules! reexport_proc_macro_attributes { + ( + $( + $(#[$meta:meta])* + $impl_path:path as $macro_name:ident + )* + ) => { + $( + $(#[$meta])* + #[proc_macro_attribute] + pub fn $macro_name( + attr: ::proc_macro::TokenStream, + item: ::proc_macro::TokenStream, + ) -> ::proc_macro::TokenStream { + $impl_path(attr.into(), item.into()).into() + } + )* + } +} +pub(crate) use reexport_proc_macro_attributes; diff --git a/packages/macro-rs/macros/src/lib.rs b/packages/macro-rs/macros/src/lib.rs new file mode 100644 index 0000000000..533f058a96 --- /dev/null +++ b/packages/macro-rs/macros/src/lib.rs @@ -0,0 +1,81 @@ +mod helper; +use helper::*; + +/// Reads the version field in the project root package.json at compile time. +/// +/// # Example +/// You can get a compile-time constant version number using this macro: +/// ``` +/// # use macros::read_version_from_package_json; +/// // VERSION == "YYYYMMDD" (or "YYYYMMDD-X") +/// const VERSION: &str = read_version_from_package_json!(); +/// ``` +#[proc_macro] +pub fn read_version_from_package_json(_item: proc_macro::TokenStream) -> proc_macro::TokenStream { + #[derive(serde::Deserialize)] + struct PackageJson { + version: String, + } + + let file = std::fs::File::open("package.json").expect("Failed to open package.json"); + let json: PackageJson = serde_json::from_reader(file).unwrap(); + let version = &json.version; + + quote!(#version).into() +} + +define_wrapper_proc_macro_attributes! { + /// Exports an enum to TypeScript, and derive [Clone]. + /// + /// You need this macro because [`napi_derive::napi`](https://docs.rs/napi-derive/latest/napi_derive/attr.napi.html) + /// automatically derives the [Clone] trait for enums and causes conflicts. + /// + /// This is a wrapper of [`napi_derive::napi`](https://docs.rs/napi-derive/latest/napi_derive/attr.napi.html) + /// that expands to + /// ```no_run + /// #[cfg_attr(not(feature = "napi"), derive(Clone))] + /// #[cfg_attr(feature = "napi", napi_derive::napi(attr))] + /// # enum E {} // to work around doc test compilation error + /// ``` + /// where `attr` is given attribute(s). + derive_clone_and_export(attr, item) { + #[cfg_attr(not(feature = "napi"), derive(Clone))] + #[cfg_attr(feature = "napi", napi_derive::napi(#attr))] + #item + } + + /// Exports a function, struct, enum, const, etc. to TypeScript. + /// + /// This is a wrapper of [macro@napi] that expands to + /// ```no_run + /// #[cfg_attr(feature = "napi", macros::napi(attr))] + /// # fn f() {} // to work around doc test compilation error + /// ``` + /// where `attr` is given attribute(s). See [macro@napi] for more details. + export(attr, item) { + #[cfg_attr(feature = "napi", macros::napi(#attr))] + #item + } + + /// Exports a function, struct, enum, const, etc. to TypeScript + /// and make it unable to use in Rust. + /// + /// This is a wrapper of [macro@napi] that expands to + /// ```no_run + /// #[cfg(feature = "napi")] + /// #[macros::napi(attr)] + /// # fn f() {} // to work around doc test compilation error + /// ``` + /// where `attr` is given attribute(s). See [macro@napi] for more details. + ts_export(attr, item) { + #[cfg(feature = "napi")] + #[macros::napi(#attr)] + #item + } +} + +reexport_proc_macro_attributes! { + /// Creates an extra wrapper function for [napi_derive](https://docs.rs/napi-derive/latest/napi_derive/). + /// See [macros_impl::napi::napi] for details. + macros_impl::napi::napi as napi +} diff --git a/packages/macro-rs/src/lib.rs b/packages/macro-rs/src/lib.rs deleted file mode 100644 index 65ad817e05..0000000000 --- a/packages/macro-rs/src/lib.rs +++ /dev/null @@ -1,680 +0,0 @@ -use convert_case::{Case, Casing}; -use proc_macro2::{TokenStream, TokenTree}; -use quote::{quote, ToTokens}; - -/// Read the version field in the project root package.json at compile time -/// -/// # Example -/// You can get a compile-time constant version number using this macro: -/// ``` -/// # use macro_rs::read_version_from_package_json; -/// // VERSION == "YYYYMMDD" (or "YYYYMMDD-X") -/// const VERSION: &str = read_version_from_package_json!(); -/// ``` -#[proc_macro] -pub fn read_version_from_package_json(_item: proc_macro::TokenStream) -> proc_macro::TokenStream { - #[derive(serde::Deserialize)] - struct PackageJson { - version: String, - } - - let file = std::fs::File::open("package.json").expect("Failed to open package.json"); - let json: PackageJson = serde_json::from_reader(file).unwrap(); - let version = &json.version; - - quote! { #version }.into() -} - -/// Export an enum to TypeScript, and derive [Clone]. -/// -/// You need this macro because [`napi_derive::napi`](https://docs.rs/napi-derive/latest/napi_derive/attr.napi.html) -/// automatically derives the [Clone] trait for enums and causes conflicts. -/// -/// This is a wrapper of [`napi_derive::napi`](https://docs.rs/napi-derive/latest/napi_derive/attr.napi.html) -/// that expands to -/// ```no_run -/// #[cfg_attr(not(feature = "napi"), derive(Clone))] -/// #[cfg_attr(feature = "napi", napi_derive::napi(attr))] -/// # enum E {} // to work around doc test compilation error -/// ``` -/// where `attr` is given attribute(s). -#[proc_macro_attribute] -pub fn derive_clone_and_export( - attr: proc_macro::TokenStream, - item: proc_macro::TokenStream, -) -> proc_macro::TokenStream { - let attr: TokenStream = attr.into(); - let item: TokenStream = item.into(); - - quote! { - #[cfg_attr(not(feature = "napi"), derive(Clone))] - #[cfg_attr(feature = "napi", napi_derive::napi(#attr))] - #item - } - .into() -} - -/// Export a function, struct, enum, const, etc. to TypeScript. -/// -/// This is a wrapper of [macro@napi] that expands to -/// ```no_run -/// #[cfg_attr(feature = "napi", macro_rs::napi(attr))] -/// # fn f() {} // to work around doc test compilation error -/// ``` -/// where `attr` is given attribute(s). See [macro@napi] for more details. -#[proc_macro_attribute] -pub fn export( - attr: proc_macro::TokenStream, - item: proc_macro::TokenStream, -) -> proc_macro::TokenStream { - let attr: TokenStream = attr.into(); - let item: TokenStream = item.into(); - - quote! { - #[cfg_attr(feature = "napi", macro_rs::napi(#attr))] - #item - } - .into() -} - -/// Export a function, struct, enum, const, etc. to TypeScript -/// and make it unable to use in Rust. -/// -/// This is a wrapper of [macro@napi] that expands to -/// ```no_run -/// #[cfg(feature = "napi")] -/// #[macro_rs::napi(attr)] -/// # fn f() {} // to work around doc test compilation error -/// ``` -/// where `attr` is given attribute(s). See [macro@napi] for more details. -#[proc_macro_attribute] -pub fn ts_export( - attr: proc_macro::TokenStream, - item: proc_macro::TokenStream, -) -> proc_macro::TokenStream { - let attr: TokenStream = attr.into(); - let item: TokenStream = item.into(); - - quote! { - #[cfg(feature = "napi")] - #[macro_rs::napi(#attr)] - #item - } - .into() -} - -/// Creates an extra wrapper function for [napi_derive](https://docs.rs/napi-derive/latest/napi_derive/). -/// -/// The macro is simply converted into `napi_derive::napi(...)` -/// if it is not applied to a function. -/// -/// The macro sets the following attributes by default if not specified: -/// - `use_nullable = true` (if `object` or `constructor` attribute is specified) -/// - `js_name` to the camelCase version of the original function name (for functions) -/// -/// 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 [`Vec`] -/// - `&T` and `&mut T` are converted to `T` -/// - Other `T` remains `T` -/// -/// In addition, return type [`Result`] and [`Result`] are converted to [`napi::Result`](https://docs.rs/napi/latest/napi/type.Result.html). -/// Note that `E` must implement [`std::string::ToString`] trait. -/// -/// # Examples -/// ## Applying the macro to a struct -/// ``` -/// #[macro_rs::napi(object)] -/// struct Person { -/// id: i32, -/// name: String, -/// } -/// ``` -/// simply becomes -/// ``` -/// #[napi_derive::napi(use_nullable = true, object)] -/// struct Person { -/// id: i32, -/// name: String, -/// } -/// ``` -/// -/// ## Function with explicitly specified `js_name` -/// ``` -/// #[macro_rs::napi(js_name = "add1")] -/// pub fn add_one(x: i32) -> i32 { -/// x + 1 -/// } -/// ``` -/// generates -/// ``` -/// # pub fn add_one(x: i32) -> i32 { -/// # x + 1 -/// # } -/// #[napi_derive::napi(js_name = "add1",)] -/// pub fn add_one_napi(x: i32) -> i32 { -/// add_one(x) -/// } -/// ``` -/// -/// ## Function with `i32` argument -/// ``` -/// #[macro_rs::napi] -/// pub fn add_one(x: i32) -> i32 { -/// x + 1 -/// } -/// ``` -/// generates -/// ``` -/// # 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) -/// } -/// ``` -/// -/// ## Function with `&str` argument -/// ``` -/// #[macro_rs::napi] -/// pub fn concatenate_string(str1: &str, str2: &str) -> String { -/// str1.to_owned() + str2 -/// } -/// ``` -/// generates -/// ``` -/// # 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) -/// } -/// ``` -/// -/// ## Function with `&[String]` argument -/// ``` -/// #[macro_rs::napi] -/// pub fn string_array_length(array: &[String]) -> u32 { -/// array.len() as u32 -/// } -/// ``` -/// generates -/// ``` -/// # pub fn string_array_length(array: &[String]) -> u32 { -/// # array.len() as u32 -/// # } -/// #[napi_derive::napi(js_name = "stringArrayLength",)] -/// pub fn string_array_length_napi(array: Vec) -> u32 { -/// string_array_length(&array) -/// } -/// ``` -/// -/// ## Function with `Result` return type -/// ```ignore -/// #[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 { -/// match divisor { -/// 0 => Err(IntegerDivisionError::DividedByZero), -/// _ => match dividend % divisor { -/// 0 => Ok(dividend / divisor), -/// remainder => Err(IntegerDivisionError::NotDivisible(remainder)), -/// }, -/// } -/// } -/// ``` -/// generates -/// ```ignore -/// # #[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 { -/// # 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 { -/// integer_divide(dividend, divisor).map_err(|err| napi::Error::from_reason(crate::util::error_chain::format_error(&err))) -/// } -/// ``` -#[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(macro_attr: TokenStream, item: TokenStream) -> TokenStream { - let macro_attr_tokens: Vec = macro_attr.clone().into_iter().collect(); - // generated extra macro attr TokenStream (prepended before original input `macro_attr`) - let mut extra_macro_attr = TokenStream::new(); - - let item: syn::Item = - syn::parse2(item).expect("Failed to parse input TokenStream to syn::Item"); - - // handle non-functions - let syn::Item::Fn(item_fn) = item else { - // append `use_nullable = true` if `object` or `constructor` present but not `use_nullable` - if macro_attr_tokens.iter().any(|token| { - matches!(token, TokenTree::Ident(ident) if ident == "object" || ident == "constructor") - }) && !macro_attr_tokens.iter().any(|token| { - matches!(token, TokenTree::Ident(ident) if ident == "use_nullable") - }) { - quote! { use_nullable = true, }.to_tokens(&mut extra_macro_attr); - } - return quote! { - #[napi_derive::napi(#extra_macro_attr #macro_attr)] - #item - }; - }; - - // handle functions - let ident = &item_fn.sig.ident; - 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::::new(); - - // append "_napi" to function name - item_fn_sig.ident = syn::parse_str(&format!("{}_napi", &ident)).unwrap(); - - // append `.await` to function call in async function - if item_fn_sig.asyncness.is_some() { - function_call_modifiers.push(quote! { - .await - }); - } - - // convert return type `...::Result` to `napi::Result` - 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 from Result - 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(crate::util::error_chain::format_error(&err))) - }); - } - }; - - // arguments in function call - let called_args: Vec = 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 - *arg.ty = syn::Type::Verbatim(match &*r.elem { - syn::Type::Slice(slice) => { - let ty = &*slice.elem; - quote! { Vec<#ty> } - } - _ => { - let elem_tokens = r.elem.to_token_stream(); - 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(); - - // handle macro attr - // append js_name if not specified - if !macro_attr_tokens - .iter() - .any(|token| matches!(token, TokenTree::Ident(ident) if ident == "js_name")) - { - let js_name = ident.to_string().to_case(Case::Camel); - quote! { js_name = #js_name, }.to_tokens(&mut extra_macro_attr); - } - - quote! { - #item_fn - - #[napi_derive::napi(#extra_macro_attr #macro_attr)] - #(#item_fn_attrs)* - #item_fn_vis #item_fn_sig { - #ident(#(#called_args),*) - #(#function_call_modifiers)* - } - } -} - -#[cfg(test)] -mod tests { - use proc_macro2::TokenStream; - use quote::quote; - - macro_rules! test_macro_becomes { - ($source:expr, $generated:expr) => { - assert_eq!( - super::napi_impl(TokenStream::new(), $source).to_string(), - $generated.to_string(), - ) - }; - ($macro_attr:expr, $source:expr, $generated:expr) => { - assert_eq!( - super::napi_impl($macro_attr, $source).to_string(), - $generated.to_string(), - ) - }; - } - - macro_rules! test_macro_generates { - ($source:expr, $generated:expr) => { - assert_eq!( - super::napi_impl(TokenStream::new(), $source).to_string(), - format!("{} {}", $source, $generated), - ) - }; - ($macro_attr:expr, $source:expr, $generated:expr) => { - assert_eq!( - super::napi_impl($macro_attr, $source).to_string(), - format!("{} {}", $source, $generated), - ) - }; - } - - #[test] - fn primitive_argument() { - test_macro_generates!( - quote! { - pub fn add_one(x: i32) -> i32 { - x + 1 - } - }, - quote! { - #[napi_derive::napi(js_name = "addOne", )] - pub fn add_one_napi(x: i32) -> i32 { - add_one(x) - } - } - ); - } - - #[test] - fn str_ref_argument() { - test_macro_generates!( - quote! { - pub fn concatenate_string(str1: &str, str2: &str) -> String { - str1.to_owned() + str2 - } - }, - quote! { - #[napi_derive::napi(js_name = "concatenateString", )] - pub fn concatenate_string_napi(str1: String, str2: String) -> String { - concatenate_string(&str1, &str2) - } - } - ); - } - - #[test] - fn mut_ref_argument() { - test_macro_generates!( - quote! { - pub fn append_string_and_clone( - base_str: &mut String, - appended_str: &str, - ) -> String { - base_str.push_str(appended_str); - base_str.to_owned() - } - }, - quote! { - #[napi_derive::napi(js_name = "appendStringAndClone", )] - pub fn append_string_and_clone_napi( - mut base_str: String, - appended_str: String, - ) -> String { - append_string_and_clone(&mut base_str, &appended_str) - } - } - ); - } - - #[test] - fn result_return_type() { - test_macro_generates!( - quote! { - pub fn integer_divide( - dividend: i64, - divisor: i64, - ) -> Result { - 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 { - integer_divide(dividend, divisor) - .map_err(|err| napi::Error::from_reason(crate::util::error_chain::format_error(&err))) - } - } - ); - } - - #[test] - fn async_function() { - test_macro_generates!( - quote! { - pub async fn async_add_one(x: i32) -> i32 { - x + 1 - } - }, - quote! { - #[napi_derive::napi(js_name = "asyncAddOne", )] - pub async fn async_add_one_napi(x: i32) -> i32 { - async_add_one(x) - .await - } - } - ) - } - - #[test] - fn slice_type() { - test_macro_generates!( - quote! { - pub fn string_array_length(array: &[String]) -> u32 { - array.len() as u32 - } - }, - quote! { - #[napi_derive::napi(js_name = "stringArrayLength", )] - pub fn string_array_length_napi(array: Vec) -> u32 { - string_array_length(&array) - } - } - ) - } - - #[test] - fn object() { - test_macro_becomes!( - quote! { object }, - quote! { - struct Person { - id: i32, - name: Option, - } - }, - quote! { - #[napi_derive::napi(use_nullable = true, object)] - struct Person { - id: i32, - name: Option, - } - } - ) - } - - #[test] - fn object_with_explicitly_set_use_nullable() { - test_macro_becomes!( - quote! { object, use_nullable = false }, - quote! { - struct Person { - id: i32, - name: Option, - } - }, - quote! { - #[napi_derive::napi(object, use_nullable = false)] - struct Person { - id: i32, - name: Option, - } - } - ) - } - - #[test] - fn macro_attr() { - test_macro_generates!( - quote! { - ts_return_type = "number" - }, - quote! { - pub fn add_one(x: i32) -> i32 { - x + 1 - } - }, - quote! { - #[napi_derive::napi(js_name = "addOne", ts_return_type = "number")] - pub fn add_one_napi(x: i32) -> i32 { - add_one(x) - } - } - ) - } - - #[test] - fn explicitly_specified_js_name() { - test_macro_generates!( - quote! { - js_name = "add1" - }, - quote! { - pub fn add_one(x: i32) -> i32 { - x + 1 - } - }, - quote! { - #[napi_derive::napi(js_name = "add1")] - pub fn add_one_napi(x: i32) -> i32 { - add_one(x) - } - } - ) - } - - #[test] - fn explicitly_specified_js_name_and_other_macro_attr() { - test_macro_generates!( - quote! { ts_return_type = "number", js_name = "add1" }, - quote! { - pub fn add_one(x: i32) -> i32 { - x + 1 - } - }, - quote! { - #[napi_derive::napi(ts_return_type = "number", js_name = "add1")] - pub fn add_one_napi(x: i32) -> i32 { - add_one(x) - } - } - ) - } -}