refactor: macro-rs -> macros + macros_impl

Co-authored-by: naskya <m@naskya.net>
This commit is contained in:
sup39 2024-06-12 00:49:22 +08:00
parent 4e36d22164
commit 0cb4fbf6b4
No known key found for this signature in database
GPG key ID: 111C00916C1641E5
16 changed files with 780 additions and 703 deletions

View file

@ -290,7 +290,7 @@ cargo:doc:
- cp ci/cargo/config.toml /usr/local/cargo/config.toml - cp ci/cargo/config.toml /usr/local/cargo/config.toml
script: script:
- cargo doc --document-private-items - 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 '<meta http-equiv="refresh" content="0; url=%s">' 'backend_rs' > target/doc/index.html - printf '<meta http-equiv="refresh" content="0; url=%s">' 'backend_rs' > target/doc/index.html
- cd target/doc - cd target/doc
- npx --yes netlify-cli deploy --prod --site="${CARGO_DOC_SITE_ID}" --dir=. - npx --yes netlify-cli deploy --prod --site="${CARGO_DOC_SITE_ID}" --dir=.

18
Cargo.lock generated
View file

@ -210,7 +210,7 @@ dependencies = [
"idna", "idna",
"image", "image",
"isahc", "isahc",
"macro-rs", "macros",
"napi", "napi",
"napi-build", "napi-build",
"napi-derive", "napi-derive",
@ -1718,18 +1718,24 @@ dependencies = [
] ]
[[package]] [[package]]
name = "macro-rs" name = "macros"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"convert_case", "macros-impl",
"napi",
"napi-derive",
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde", "serde",
"serde_json", "serde_json",
]
[[package]]
name = "macros-impl"
version = "0.0.0"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"syn 2.0.66", "syn 2.0.66",
"thiserror",
] ]
[[package]] [[package]]

View file

@ -1,9 +1,10 @@
[workspace] [workspace]
members = ["packages/backend-rs", "packages/macro-rs"] members = ["packages/backend-rs", "packages/macro-rs/macros", "packages/macro-rs/macros-impl"]
resolver = "2" resolver = "2"
[workspace.dependencies] [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 = { git = "https://github.com/napi-rs/napi-rs.git", rev = "ca2cd5c35a0c39ec4a94e93c6c5695b681046df2" }
napi-derive = "2.16.5" napi-derive = "2.16.5"

View file

@ -17,8 +17,7 @@ COPY Cargo.toml Cargo.toml
COPY Cargo.lock Cargo.lock COPY Cargo.lock Cargo.lock
COPY packages/backend-rs/Cargo.toml packages/backend-rs/Cargo.toml COPY packages/backend-rs/Cargo.toml packages/backend-rs/Cargo.toml
COPY packages/backend-rs/src/lib.rs packages/backend-rs/src/ 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 packages/macro-rs/
COPY packages/macro-rs/src/lib.rs packages/macro-rs/src/
# Configure pnpm, and install backend-rs dependencies # Configure pnpm, and install backend-rs dependencies
RUN corepack enable && corepack prepare pnpm@latest --activate && pnpm --filter backend-rs install 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 in the rest of the rust files
COPY packages/backend-rs packages/backend-rs/ COPY packages/backend-rs packages/backend-rs/
# COPY packages/macro-rs packages/macro-rs/
# Compile backend-rs # Compile backend-rs
RUN NODE_ENV='production' pnpm run --filter backend-rs build RUN NODE_ENV='production' pnpm run --filter backend-rs build

View file

@ -12,7 +12,7 @@ napi = ["dep:napi", "dep:napi-derive", "dep:napi-build"]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]
[dependencies] [dependencies]
macro-rs = { workspace = true } macros = { workspace = true }
napi = { workspace = true, optional = true, features = ["chrono_date", "napi4", "serde-json", "tokio_rt"] } napi = { workspace = true, optional = true, features = ["chrono_date", "napi4", "serde-json", "tokio_rt"] }
napi-derive = { workspace = true, optional = true } napi-derive = { workspace = true, optional = true }

View file

@ -4,7 +4,7 @@ use once_cell::sync::Lazy;
use serde::Deserialize; use serde::Deserialize;
use std::{env, fs}; 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)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]

View file

@ -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 config;
pub mod database; pub mod database;

View file

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

View file

@ -0,0 +1,2 @@
pub mod napi;
mod util;

View file

@ -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>`]
/// - `&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>`](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<String>) -> u32 {
/// string_array_length(&array)
/// }
/// # });
/// ```
///
/// ## Function with `Result<T, E>` 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<i64, IntegerDivisionError> {
/// 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<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(crate::util::error_chain::format_error(&err)))
/// }
/// # });
/// ```
///
pub fn napi(macro_attr: TokenStream, item: TokenStream) -> TokenStream {
let macro_attr_tokens: Vec<TokenTree> = 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::<TokenStream>::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<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(crate::util::error_chain::format_error(&err)))
});
}
};
// 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
*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<i64, IntegerDivisionError> {
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<i64> {
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<String>) -> u32 {
string_array_length(&array)
}
}
object_with_explicitly_set_use_nullable: {
#[macros::napi(object, use_nullable = false)]
struct Person {
id: i32,
name: Option<String>,
}
} becomes {
#[napi_derive::napi(object, use_nullable = false)]
struct Person {
id: i32,
name: Option<String>,
}
}
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)
}
}
}

View file

@ -0,0 +1,3 @@
//! Utilities for developing procedural macros
mod tester;

View file

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

View file

@ -1,5 +1,5 @@
[package] [package]
name = "macro-rs" name = "macros"
version = "0.0.0" version = "0.0.0"
edition = "2021" edition = "2021"
rust-version = "1.74" rust-version = "1.74"
@ -8,14 +8,9 @@ rust-version = "1.74"
proc-macro = true proc-macro = true
[dependencies] [dependencies]
convert_case = { workspace = true } macros-impl = { workspace = true }
proc-macro2 = { workspace = true } proc-macro2 = { workspace = true }
quote = { workspace = true } quote = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["std"] } 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"] }

View file

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

View file

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

View file

@ -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>`]
/// - `&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>`](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<String>) -> u32 {
/// string_array_length(&array)
/// }
/// ```
///
/// ## Function with `Result<T, E>` 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<i64, IntegerDivisionError> {
/// 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<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(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<TokenTree> = 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::<TokenStream>::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<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(crate::util::error_chain::format_error(&err)))
});
}
};
// 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
*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<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(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<String>) -> u32 {
string_array_length(&array)
}
}
)
}
#[test]
fn object() {
test_macro_becomes!(
quote! { object },
quote! {
struct Person {
id: i32,
name: Option<String>,
}
},
quote! {
#[napi_derive::napi(use_nullable = true, object)]
struct Person {
id: i32,
name: Option<String>,
}
}
)
}
#[test]
fn object_with_explicitly_set_use_nullable() {
test_macro_becomes!(
quote! { object, use_nullable = false },
quote! {
struct Person {
id: i32,
name: Option<String>,
}
},
quote! {
#[napi_derive::napi(object, use_nullable = false)]
struct Person {
id: i32,
name: Option<String>,
}
}
)
}
#[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)
}
}
)
}
}