diff --git a/Cargo.lock b/Cargo.lock index 7f2140d..95b7a9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,24 +44,66 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys", +] + [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "gimli" version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +[[package]] +name = "insta" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "similar", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "memchr" version = "2.7.4" @@ -120,6 +162,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rust-format" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60e7c00b6c3bf5e38a880eec01d7e829d12ca682079f8238a464def3c4b31627" + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -140,23 +188,52 @@ name = "servify_macro" version = "0.1.0" dependencies = [ "case", + "insta", "pretty_assertions", "proc-macro2", "quote", + "rust-format", "syn", + "thiserror", ] +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" + [[package]] name = "syn" -version = "2.0.77" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3c6efbfc763e64eb85c11c25320f0737cb7364c4b6336db90aa9ebe27a0bbd" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio" version = "1.40.0" @@ -185,6 +262,15 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/servify_macro/Cargo.toml b/servify_macro/Cargo.toml index 2f182a8..192f8ce 100644 --- a/servify_macro/Cargo.toml +++ b/servify_macro/Cargo.toml @@ -19,4 +19,7 @@ quote = "1.0.37" syn = { version = "2.0.77", features = ["full"] } [dev-dependencies] +insta = "1.41.1" pretty_assertions = "1.4.1" +rust-format = "0.3.4" +thiserror = "1.0.67" diff --git a/servify_macro/src/export_macro/args.rs b/servify_macro/src/export_macro/args.rs new file mode 100644 index 0000000..d185ed1 --- /dev/null +++ b/servify_macro/src/export_macro/args.rs @@ -0,0 +1,10 @@ +use syn::parse::{Parse, ParseStream}; + +/// Represents the arguments of the `#[servify::export]` attribute. +pub(crate) struct ServifyExportArgs {} + +impl Parse for ServifyExportArgs { + fn parse(_input: ParseStream) -> syn::Result { + Ok(Self {}) + } +} diff --git a/servify_macro/src/export_macro/errors.rs b/servify_macro/src/export_macro/errors.rs new file mode 100644 index 0000000..896621c --- /dev/null +++ b/servify_macro/src/export_macro/errors.rs @@ -0,0 +1,8 @@ +pub(super) const ERR_MULTIPLE_FN_IN_EXPORT: &str = + "Only one function can be exported in one impl block with servify::export"; + +pub(super) const ERR_NO_FN_IN_EXPORT: &str = + "One function must be exported in one impl block with servify::export"; + +pub(super) const ERR_UNEXPECTED_ITEM_IN_EXPORT: &str = + "Supported items are only exporting function, associated function, associated constants, and Request struct"; diff --git a/servify_macro/src/export_macro/mod.rs b/servify_macro/src/export_macro/mod.rs new file mode 100644 index 0000000..851b787 --- /dev/null +++ b/servify_macro/src/export_macro/mod.rs @@ -0,0 +1,63 @@ +mod args; +mod errors; +mod tests; +mod to_token; + +use proc_macro2::Span; +use syn::parse::{Parse, ParseStream}; +use syn::{braced, Ident, ImplItemFn, Token, TypePath}; + +use crate::export_macro::errors::{ + ERR_MULTIPLE_FN_IN_EXPORT, ERR_NO_FN_IN_EXPORT, ERR_UNEXPECTED_ITEM_IN_EXPORT, +}; + +pub(crate) use self::args::ServifyExportArgs; + +/// Represents the `#[servify::export]` content. +pub(crate) struct ServifyExport { + service_module_path: TypePath, + fn_item: ImplItemFn, + fn_name: Ident, +} + +impl Parse for ServifyExport { + fn parse(input: ParseStream) -> syn::Result { + let content; + let mut fn_item = None; + + // Parse head impl block + let _: Token![impl] = input.parse()?; + let module_path: TypePath = input.parse()?; + braced!(content in input); + + // Parse all items in the export block + while !content.is_empty() { + if let Ok(item) = content.parse::() { + if fn_item.is_some() { + return Err(syn::Error::new_spanned(item, ERR_MULTIPLE_FN_IN_EXPORT)); + } + + fn_item.replace(item); + continue; + } + + // Error if there is any other item in the export block + return Err(syn::Error::new_spanned( + content.parse::()?, + ERR_UNEXPECTED_ITEM_IN_EXPORT, + )); + } + + // Ensure that there is a function in the export block + let fn_item = + fn_item.ok_or_else(|| syn::Error::new(Span::call_site(), ERR_NO_FN_IN_EXPORT))?; + + // TODO: combine multiple error + + Ok(Self { + service_module_path: module_path, + fn_name: fn_item.sig.ident.clone(), + fn_item, + }) + } +} diff --git a/servify_macro/src/export_macro/snapshots/servify_macro__export_macro__tests__snapshot_export_all_written.snap b/servify_macro/src/export_macro/snapshots/servify_macro__export_macro__tests__snapshot_export_all_written.snap new file mode 100644 index 0000000..774040f --- /dev/null +++ b/servify_macro/src/export_macro/snapshots/servify_macro__export_macro__tests__snapshot_export_all_written.snap @@ -0,0 +1,5 @@ +--- +source: servify_macro/src/export_macro/tests.rs +expression: "test(quote! {},\n quote! {\n impl super::counter\n {\n pub fn increment(&mut self, amount: u32) -> u32\n { self.count += amount; self.amount }\n }\n }).unwrap()" +--- +mod counter_increment {} diff --git a/servify_macro/src/export_macro/snapshots/servify_macro__export_macro__tests__snapshot_export_unwanted_struct.snap b/servify_macro/src/export_macro/snapshots/servify_macro__export_macro__tests__snapshot_export_unwanted_struct.snap new file mode 100644 index 0000000..3242da1 --- /dev/null +++ b/servify_macro/src/export_macro/snapshots/servify_macro__export_macro__tests__snapshot_export_unwanted_struct.snap @@ -0,0 +1,5 @@ +--- +source: servify_macro/src/export_macro/tests.rs +expression: "test(quote! {},\n quote! {\n impl super::counter\n {\n pub fn increment(&mut self, amount: u32) -> u32\n { self.count += amount; self.amount } struct\n ThisMustBeUnexpected {};\n }\n }).unwrap_err()" +--- +Failed to parse item: :: core :: compile_error ! { "Supported items within the servify::export macro are only exporting function, associated function, associated constants, and Request struct" } diff --git a/servify_macro/src/export_macro/tests.rs b/servify_macro/src/export_macro/tests.rs new file mode 100644 index 0000000..8c8a21b --- /dev/null +++ b/servify_macro/src/export_macro/tests.rs @@ -0,0 +1,65 @@ +#![cfg(test)] +use insta::assert_snapshot; +use proc_macro2::TokenStream; +use quote::quote; +use rust_format::{Formatter, RustFmt}; +use thiserror::Error; + +use crate::export_macro::{ServifyExport, ServifyExportArgs}; + +#[derive(Debug, Error)] +enum Error { + #[error("Failed to parse args: {0}")] + ParseArgs(TokenStream), + #[error("Failed to parse item: {0}")] + ParseItem(TokenStream), + #[error("Failed to format: {0}")] + RustFmt(#[from] rust_format::Error), +} + +fn test(args: TokenStream, item: TokenStream) -> Result { + let args = syn::parse2::(args) + .map_err(|e| e.to_compile_error()) + .map_err(Error::ParseArgs)?; + let item = syn::parse2::(item) + .map_err(|e| e.to_compile_error()) + .map_err(Error::ParseItem)?; + + let generated = item.to_tokens(args).to_string(); + + let formatted = RustFmt::default().format_str(generated)?; + Ok(formatted) +} + +#[test] +fn test_snapshot_export_all_written() { + assert_snapshot!(test( + quote! {}, + quote! { + impl super::counter { + pub fn increment(&mut self, amount: u32) -> u32 { + self.count += amount; + self.amount + } + } + } + ) + .unwrap()); +} + +#[test] +fn test_snapshot_export_unwanted_struct() { + assert_snapshot!(test( + quote! {}, + quote! { + impl super::counter { + pub fn increment(&mut self, amount: u32) -> u32 { + self.count += amount; + self.amount + } + struct ThisMustBeUnexpected { }; + } + } + ) + .unwrap_err()); +} diff --git a/servify_macro/src/export_macro/to_token.rs b/servify_macro/src/export_macro/to_token.rs new file mode 100644 index 0000000..6e36b84 --- /dev/null +++ b/servify_macro/src/export_macro/to_token.rs @@ -0,0 +1,24 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::Ident; + +use crate::util::ident_ext::IdentExt as _; + +use super::{ServifyExport, ServifyExportArgs}; + +impl ServifyExport { + pub(crate) fn to_tokens(self, args: ServifyExportArgs) -> TokenStream { + let ServifyExport { + service_module_path, + fn_item, + fn_name, + } = self; + + let service_name = &service_module_path.path.segments.last().unwrap().ident; + let mod_name = Ident::new_with_call_site(&format!("{service_name}_{fn_name}")); + + quote! { + mod #mod_name { } + } + } +} diff --git a/servify_macro/src/lib.rs b/servify_macro/src/lib.rs index 83c8c0a..57db8bb 100644 --- a/servify_macro/src/lib.rs +++ b/servify_macro/src/lib.rs @@ -1 +1,15 @@ -mod util; +use proc_macro::TokenStream; + +use export_macro::ServifyExport; +use export_macro::ServifyExportArgs; + +pub(crate) mod export_macro; +pub(crate) mod util; + +#[proc_macro_attribute] +pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream { + let attr = syn::parse_macro_input!(attr as ServifyExportArgs); + syn::parse_macro_input!(item as ServifyExport) + .to_tokens(attr) + .into() +} diff --git a/servify_macro/src/util/ident_ext.rs b/servify_macro/src/util/ident_ext.rs new file mode 100644 index 0000000..5e3431c --- /dev/null +++ b/servify_macro/src/util/ident_ext.rs @@ -0,0 +1,12 @@ +use proc_macro2::Span; +use syn::Ident; + +pub(crate) trait IdentExt { + fn new_with_call_site(name: &str) -> Ident; +} + +impl IdentExt for Ident { + fn new_with_call_site(name: &str) -> Ident { + Ident::new(name, Span::call_site()) + } +} diff --git a/servify_macro/src/util/mod.rs b/servify_macro/src/util/mod.rs index b1f3fab..3dc489e 100644 --- a/servify_macro/src/util/mod.rs +++ b/servify_macro/src/util/mod.rs @@ -1,2 +1,3 @@ -pub mod return_type_ext; -pub mod type_path_ext; +pub(crate) mod ident_ext; +pub(crate) mod return_type_ext; +pub(crate) mod type_path_ext;