refactor: macro-rs -> macros + macros_impl
Co-authored-by: naskya <m@naskya.net>
This commit is contained in:
parent
4e36d22164
commit
0cb4fbf6b4
16 changed files with 780 additions and 703 deletions
|
@ -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 '<meta http-equiv="refresh" content="0; url=%s">' 'backend_rs' > target/doc/index.html
|
||||
- cd target/doc
|
||||
- npx --yes netlify-cli deploy --prod --site="${CARGO_DOC_SITE_ID}" --dir=.
|
||||
|
|
18
Cargo.lock
generated
18
Cargo.lock
generated
|
@ -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]]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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;
|
||||
|
|
11
packages/macro-rs/macros-impl/Cargo.toml
Normal file
11
packages/macro-rs/macros-impl/Cargo.toml
Normal 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"] }
|
2
packages/macro-rs/macros-impl/src/lib.rs
Normal file
2
packages/macro-rs/macros-impl/src/lib.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod napi;
|
||||
mod util;
|
451
packages/macro-rs/macros-impl/src/napi.rs
Normal file
451
packages/macro-rs/macros-impl/src/napi.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
3
packages/macro-rs/macros-impl/src/util/mod.rs
Normal file
3
packages/macro-rs/macros-impl/src/util/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
//! Utilities for developing procedural macros
|
||||
|
||||
mod tester;
|
121
packages/macro-rs/macros-impl/src/util/tester.rs
Normal file
121
packages/macro-rs/macros-impl/src/util/tester.rs
Normal 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);
|
||||
}
|
||||
)*
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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"] }
|
88
packages/macro-rs/macros/src/helper.rs
Normal file
88
packages/macro-rs/macros/src/helper.rs
Normal 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;
|
81
packages/macro-rs/macros/src/lib.rs
Normal file
81
packages/macro-rs/macros/src/lib.rs
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue