diff --git a/Cargo.toml b/Cargo.toml index 2c614a149..259518b2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,8 @@ salvo-rate-limiter = { version = "0.72.2", path = "crates/rate-limiter", default salvo-serde-util = { version = "0.72.2", path = "crates/serde-util", default-features = true } salvo-serve-static = { version = "0.72.2", path = "crates/serve-static", default-features = false } salvo-session = { version = "0.72.2", path = "crates/session", default-features = false } +salvo-craft = { version = "0.72.2", path = "crates/craft", default-features = false } +salvo-craft-macros = { version = "0.72.2", path = "crates/craft-macros", default-features = false } aead = "0.5" aes-gcm = "0.10" diff --git a/crates/craft-macros/Cargo.toml b/crates/craft-macros/Cargo.toml new file mode 100644 index 000000000..dca25ac53 --- /dev/null +++ b/crates/craft-macros/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "salvo-craft-macros" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +documentation = "https://docs.rs/salvo-craft-macros" +readme = "README.md" +description = "Salvo Handler modular craft macros." +keywords = ["http", "async", "web", "framework", "server"] +categories = [ + "web-programming::http-server", + "web-programming::websocket", + "network-programming", + "asynchronous", +] +authors = ["Andeya Lee "] + +[lib] +proc-macro = true + +[dependencies] +proc-macro-crate.workspace = true +proc-macro2.workspace = true +quote.workspace = true +syn = { workspace = true, features = ["full", "parsing"] } + +[dev-dependencies] +salvo = { path = "../salvo", features = ["oapi"] } +tokio.workspace = true + +[lints] +workspace = true \ No newline at end of file diff --git a/crates/craft-macros/README.md b/crates/craft-macros/README.md new file mode 100644 index 000000000..21d277675 --- /dev/null +++ b/crates/craft-macros/README.md @@ -0,0 +1,66 @@ +# salvo-craft-macros + +[`Salvo`](https://github.com/salvo-rs/salvo) `Handler` modular craft macros. + +[![Crates.io](https://img.shields.io/crates/v/salvo-craft-macros)](https://crates.io/crates/salvo-craft-macros) +[![Documentation](https://shields.io/docsrs/salvo-craft-macros)](https://docs.rs/salvo-craft-macros) + +## `#[craft]` + +`#[craft]` is an attribute macro used to batch convert methods in an `impl` block into [`Salvo`'s `Handler`](https://github.com/salvo-rs/salvo). + +```rust +use salvo::oapi::extract::*; +use salvo::prelude::*; +use salvo_craft_macros::craft; +use std::sync::Arc; + +#[tokio::main] +async fn main() { + let service = Arc::new(Service::new(1)); + let router = Router::new() + .push(Router::with_path("add1").get(service.add1())) + .push(Router::with_path("add2").get(service.add2())) + .push(Router::with_path("add3").get(Service::add3())); + let acceptor = TcpListener::new("127.0.0.1:5800").bind().await; + Server::new(acceptor).serve(router).await; +} + +#[derive(Clone)] +pub struct Service { + state: i64, +} + +#[craft] +impl Service { + fn new(state: i64) -> Self { + Self { state } + } + /// doc line 1 + /// doc line 2 + #[craft(handler)] + fn add1(&self, left: QueryParam, right: QueryParam) -> String { + (self.state + *left + *right).to_string() + } + /// doc line 3 + /// doc line 4 + #[craft(handler)] + pub(crate) fn add2( + self: ::std::sync::Arc, + left: QueryParam, + right: QueryParam, + ) -> String { + (self.state + *left + *right).to_string() + } + /// doc line 5 + /// doc line 6 + #[craft(handler)] + pub fn add3(left: QueryParam, right: QueryParam) -> String { + (*left + *right).to_string() + } +} +``` + +Sure, you can also replace `#[craft(handler)]` with `#[craft(endpoint(...))]`. + +NOTE: If the receiver of a method is `&self`, you need to implement the `Clone` trait for the type. diff --git a/crates/craft-macros/examples/add.rs b/crates/craft-macros/examples/add.rs new file mode 100644 index 000000000..dca59936b --- /dev/null +++ b/crates/craft-macros/examples/add.rs @@ -0,0 +1,55 @@ +#![allow(missing_docs)] + +use salvo::oapi::extract::*; +use salvo::prelude::*; +use salvo_craft_macros::craft; +use std::sync::Arc; + +#[tokio::main] +async fn main() { + let service = Arc::new(Service::new(1)); + let router = Router::new() + .push(Router::with_path("add1").get(service.add1())) + .push(Router::with_path("add2").get(service.add2())) + .push(Router::with_path("add3").get(Service::add3())); + let doc = OpenApi::new("Example API", "0.0.1").merge_router(&router); + let router = router + .push(doc.into_router("/api-doc/openapi.json")) + .push(SwaggerUi::new("/api-doc/openapi.json").into_router("swagger-ui")); + let acceptor = TcpListener::new("127.0.0.1:5800").bind().await; + Server::new(acceptor).serve(router).await; +} + +#[derive(Clone)] +pub struct Service { + state: i64, +} + +#[craft] +impl Service { + fn new(state: i64) -> Self { + Self { state } + } + /// doc line 1 + /// doc line 2 + #[craft(handler)] + fn add1(&self, left: QueryParam, right: QueryParam) -> String { + (self.state + *left + *right).to_string() + } + /// doc line 3 + /// doc line 4 + #[craft(endpoint)] + pub(crate) fn add2( + self: ::std::sync::Arc, + left: QueryParam, + right: QueryParam + ) -> String { + (self.state + *left + *right).to_string() + } + /// doc line 5 + /// doc line 6 + #[craft(endpoint(responses((status_code = 400, description = "Wrong request parameters."))))] + pub fn add3(left: QueryParam, right: QueryParam) -> String { + (*left + *right).to_string() + } +} diff --git a/crates/craft-macros/src/craft.rs b/crates/craft-macros/src/craft.rs new file mode 100644 index 000000000..220146fc2 --- /dev/null +++ b/crates/craft-macros/src/craft.rs @@ -0,0 +1,161 @@ +use crate::utils::salvo_crate; +use proc_macro2::{ Span, TokenStream }; +use quote::{ quote, ToTokens }; +use syn::{ + parse::Parser, + parse_quote, + Attribute, + FnArg, + Ident, + ImplItem, + ImplItemFn, + Item, + Token, + Type, +}; + +pub(crate) fn generate(input: Item) -> syn::Result { + match input { + Item::Impl(mut item_impl) => { + for item in &mut item_impl.items { + if let ImplItem::Fn(method) = item { + rewrite_method(item_impl.self_ty.clone(), method)?; + } + } + Ok(item_impl.into_token_stream()) + } + Item::Fn(_) => Ok(input.into_token_stream()), + _ => Err(syn::Error::new_spanned(input, "#[craft] must added to `impl`")), + } +} + +fn take_method_macro(item_fn: &mut ImplItemFn) -> syn::Result> { + let mut index: Option = None; + let mut new_attr: Option = None; + for (idx, attr) in &mut item_fn.attrs.iter().enumerate() { + if + !(match attr.path().segments.last() { + Some(segment) => segment.ident.to_string() == "craft", + None => false, + }) + { + continue; + } + if let Some((_, last)) = attr.to_token_stream().to_string().split_once("craft(") { + if let Some(last) = last.strip_suffix(")]") { + let ts: Option = if last == "handler" || last.starts_with("handler(") { + Some(format!("#[{}::{last}]", salvo_crate()).parse()?) + } else if last == "endpoint" || last.starts_with("endpoint(") { + Some(format!("#[{}::oapi::{last}]", salvo_crate()).parse()?) + } else { + None + }; + if let Some(ts) = ts { + new_attr = Attribute::parse_outer.parse2(ts)?.into_iter().next(); + index = Some(idx); + continue; + } + } + } + return Err( + syn::Error::new_spanned( + item_fn, + "The attribute macro #[craft] on a method must be filled with sub-attributes, such as '#[craft(handler)]', '#[craft(endpoint)]', or '#[craft(endpoint(...))]'." + ) + ); + } + if let Some(index) = index { + item_fn.attrs.remove(index); + return Ok(new_attr); + } + Ok(None) +} + +enum MethodStyle { + NoSelf, + RefSelf, + ArcSelf, +} + +impl MethodStyle { + fn from_method(method: &ImplItemFn) -> syn::Result { + let Some(recv) = method.sig.receiver() else { + return Ok(Self::NoSelf); + }; + let ty = recv.ty.to_token_stream().to_string().replace(" ", ""); + match ty.as_str() { + "&Self" => Ok(Self::RefSelf), + "Arc" | "&Arc" => Ok(Self::ArcSelf), + _ => { + if ty.ends_with("::Arc") { + Ok(Self::ArcSelf) + } else { + Err( + syn::Error::new_spanned( + method, + "#[craft] method receiver must be '&self', 'Arc' or '&Arc'" + ) + ) + } + } + } + } +} + +fn rewrite_method(self_ty: Box, method: &mut ImplItemFn) -> syn::Result<()> { + let Some(macro_attr) = take_method_macro(method)? else { + return Ok(()); + }; + method.sig.asyncness = Some(Token![async](Span::call_site())); + let salvo = salvo_crate(); + let handler = quote!(#salvo::Handler); + let method_name = method.sig.ident.clone(); + let vis = method.vis.clone(); + let mut attrs = method.attrs.clone(); + let mut new_method: ImplItemFn = match MethodStyle::from_method(method)? { + MethodStyle::NoSelf => { + method.attrs.push(macro_attr); + parse_quote! { + #vis fn #method_name() -> impl #handler { + + #method + + #method_name + } + } + } + style => { + let (receiver, output) = match style { + MethodStyle::RefSelf => { + (quote!(&self), quote!(::std::sync::Arc::new(self.clone()))) + } + MethodStyle::ArcSelf => { + (quote!(self: &::std::sync::Arc), quote!(self.clone())) + } + _ => unreachable!(), + }; + method.sig.inputs[0] = FnArg::Receiver(parse_quote!(&self)); + method.sig.ident = Ident::new("handle", Span::call_site()); + parse_quote! { + #vis fn #method_name(#receiver) -> impl #handler { + pub struct handle(::std::sync::Arc<#self_ty>); + impl ::std::ops::Deref for handle { + type Target = #self_ty; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + #macro_attr + impl handle { + #method + } + handle(#output) + } + } + } + }; + new_method.attrs.append(&mut attrs); + *method = new_method; + Ok(()) +} diff --git a/crates/craft-macros/src/lib.rs b/crates/craft-macros/src/lib.rs new file mode 100644 index 000000000..909055d8d --- /dev/null +++ b/crates/craft-macros/src/lib.rs @@ -0,0 +1,61 @@ +//! [`Salvo`](https://github.com/salvo-rs/salvo) `Handler` modular craft macros. + +mod craft; +mod utils; + +use proc_macro::TokenStream; +use syn::{ parse_macro_input, Item }; + +/// `#[craft]` is an attribute macro used to batch convert methods in an `impl` block into [`Salvo`'s `Handler`](https://github.com/salvo-rs/salvo). +/// +/// ## Example +/// ``` +/// use salvo::oapi::extract::*; +/// use salvo::prelude::*; +/// use salvo_craft_macros::craft; +/// +/// #[derive(Clone)] +/// pub struct Service { +/// state: i64, +/// } +/// +/// #[craft] +/// impl Service { +/// fn new(state: i64) -> Self { +/// Self { state } +/// } +/// /// doc line 1 +/// /// doc line 2 +/// #[salvo_craft_macros::craft(handler)] +/// fn add1(&self, left: QueryParam, right: QueryParam) -> String { +/// (self.state + *left + *right).to_string() +/// } +/// /// doc line 3 +/// /// doc line 4 +/// #[craft(handler)] +/// pub(crate) fn add2( +/// self: ::std::sync::Arc, +/// left: QueryParam, +/// right: QueryParam, +/// ) -> String { +/// (self.state + *left + *right).to_string() +/// } +/// /// doc line 5 +/// /// doc line 6 +/// #[craft(handler)] +/// pub fn add3(left: QueryParam, right: QueryParam) -> String { +/// (*left + *right).to_string() +/// } +/// } +/// ``` +/// Sure, you can also replace `#[craft(handler)]` with `#[craft(endpoint(...))]`. +/// +/// NOTE: If the receiver of a method is `&self`, you need to implement the `Clone` trait for the type. +#[proc_macro_attribute] +pub fn craft(_args: TokenStream, input: TokenStream) -> TokenStream { + let item = parse_macro_input!(input as Item); + match craft::generate(item) { + Ok(stream) => stream.into(), + Err(e) => e.to_compile_error().into(), + } +} diff --git a/crates/craft-macros/src/utils.rs b/crates/craft-macros/src/utils.rs new file mode 100644 index 000000000..e5634ce06 --- /dev/null +++ b/crates/craft-macros/src/utils.rs @@ -0,0 +1,19 @@ +use proc_macro2::Span; +use proc_macro_crate::{crate_name, FoundCrate}; +use syn::Ident; + +pub(crate) fn salvo_crate() -> syn::Ident { + match crate_name("salvo") { + Ok(salvo) => match salvo { + FoundCrate::Itself => Ident::new("salvo", Span::call_site()), + FoundCrate::Name(name) => Ident::new(&name, Span::call_site()), + }, + Err(_) => match crate_name("salvo_core") { + Ok(salvo) => match salvo { + FoundCrate::Itself => Ident::new("salvo_core", Span::call_site()), + FoundCrate::Name(name) => Ident::new(&name, Span::call_site()), + }, + Err(_) => Ident::new("salvo", Span::call_site()), + }, + } +} diff --git a/crates/craft/Cargo.toml b/crates/craft/Cargo.toml new file mode 100644 index 000000000..8ca69b4f0 --- /dev/null +++ b/crates/craft/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "salvo-craft" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +documentation = "https://docs.rs/salvo-craft" +readme = "README.md" +description = "Salvo Handler modular craft." +keywords = ["http", "async", "web", "framework", "server"] +categories = [ + "web-programming::http-server", + "web-programming::websocket", + "network-programming", + "asynchronous", +] +authors = ["Andeya Lee "] + +[dependencies] +salvo-craft-macros = { workspace = true } + +[dev-dependencies] +salvo = { path = "../salvo", features = ["oapi"] } +tokio.workspace = true + +[lints] +workspace = true diff --git a/crates/craft/README.md b/crates/craft/README.md new file mode 100644 index 000000000..bce90dc2f --- /dev/null +++ b/crates/craft/README.md @@ -0,0 +1,66 @@ +# salvo-craft + +[`Salvo`](https://github.com/salvo-rs/salvo) `Handler` modular craft macros. + +[![Crates.io](https://img.shields.io/crates/v/salvo-craft)](https://crates.io/crates/salvo-craft) +[![Documentation](https://shields.io/docsrs/salvo-craft)](https://docs.rs/salvo-craft) + +## `#[craft]` + +`#[craft]` is an attribute macro used to batch convert methods in an `impl` block into [`Salvo`'s `Handler`](https://github.com/salvo-rs/salvo). + +```rust +use salvo::oapi::extract::*; +use salvo::prelude::*; +use salvo_craft::craft; +use std::sync::Arc; + +#[tokio::main] +async fn main() { + let service = Arc::new(Service::new(1)); + let router = Router::new() + .push(Router::with_path("add1").get(service.add1())) + .push(Router::with_path("add2").get(service.add2())) + .push(Router::with_path("add3").get(Service::add3())); + let acceptor = TcpListener::new("127.0.0.1:5800").bind().await; + Server::new(acceptor).serve(router).await; +} + +#[derive(Clone)] +pub struct Service { + state: i64, +} + +#[craft] +impl Service { + fn new(state: i64) -> Self { + Self { state } + } + /// doc line 1 + /// doc line 2 + #[craft(handler)] + fn add1(&self, left: QueryParam, right: QueryParam) -> String { + (self.state + *left + *right).to_string() + } + /// doc line 3 + /// doc line 4 + #[craft(handler)] + pub(crate) fn add2( + self: ::std::sync::Arc, + left: QueryParam, + right: QueryParam, + ) -> String { + (self.state + *left + *right).to_string() + } + /// doc line 5 + /// doc line 6 + #[craft(handler)] + pub fn add3(left: QueryParam, right: QueryParam) -> String { + (*left + *right).to_string() + } +} +``` + +Sure, you can also replace `#[craft(handler)]` with `#[craft(endpoint(...))]`. + +NOTE: If the receiver of a method is `&self`, you need to implement the `Clone` trait for the type. diff --git a/crates/craft/examples/add.rs b/crates/craft/examples/add.rs new file mode 100644 index 000000000..4b93ad9d1 --- /dev/null +++ b/crates/craft/examples/add.rs @@ -0,0 +1,55 @@ +#![allow(missing_docs)] + +use salvo::oapi::extract::*; +use salvo::prelude::*; +use salvo_craft::craft; +use std::sync::Arc; + +#[tokio::main] +async fn main() { + let service = Arc::new(Service::new(1)); + let router = Router::new() + .push(Router::with_path("add1").get(service.add1())) + .push(Router::with_path("add2").get(service.add2())) + .push(Router::with_path("add3").get(Service::add3())); + let doc = OpenApi::new("Example API", "0.0.1").merge_router(&router); + let router = router + .push(doc.into_router("/api-doc/openapi.json")) + .push(SwaggerUi::new("/api-doc/openapi.json").into_router("swagger-ui")); + let acceptor = TcpListener::new("127.0.0.1:5800").bind().await; + Server::new(acceptor).serve(router).await; +} + +#[derive(Clone)] +pub struct Service { + state: i64, +} + +#[craft] +impl Service { + fn new(state: i64) -> Self { + Self { state } + } + /// doc line 1 + /// doc line 2 + #[craft(handler)] + fn add1(&self, left: QueryParam, right: QueryParam) -> String { + (self.state + *left + *right).to_string() + } + /// doc line 3 + /// doc line 4 + #[craft(endpoint)] + pub(crate) fn add2( + self: ::std::sync::Arc, + left: QueryParam, + right: QueryParam + ) -> String { + (self.state + *left + *right).to_string() + } + /// doc line 5 + /// doc line 6 + #[craft(endpoint(responses((status_code = 400, description = "Wrong request parameters."))))] + pub fn add3(left: QueryParam, right: QueryParam) -> String { + (*left + *right).to_string() + } +} diff --git a/crates/craft/src/lib.rs b/crates/craft/src/lib.rs new file mode 100644 index 000000000..a39001f3c --- /dev/null +++ b/crates/craft/src/lib.rs @@ -0,0 +1,3 @@ +//! [`Salvo`](https://github.com/salvo-rs/salvo) `Handler` modular craft. + +pub use salvo_craft_macros::*; diff --git a/crates/salvo/Cargo.toml b/crates/salvo/Cargo.toml index f1ad8f199..e50cfc459 100644 --- a/crates/salvo/Cargo.toml +++ b/crates/salvo/Cargo.toml @@ -87,6 +87,7 @@ salvo-serve-static = { workspace = true, features = ["full"], optional = true } salvo-proxy = { workspace = true, features = ["full"], optional = true } salvo-otel = { workspace = true, optional = true } salvo-oapi = { workspace = true, features = ["full"], optional = true } +salvo-craft = { workspace = true } [lints] workspace = true \ No newline at end of file diff --git a/crates/salvo/src/lib.rs b/crates/salvo/src/lib.rs index 28156ed78..70d2f351c 100644 --- a/crates/salvo/src/lib.rs +++ b/crates/salvo/src/lib.rs @@ -264,4 +264,5 @@ pub mod prelude { pub use crate::oapi::redoc::ReDoc; pub use crate::oapi::scalar::Scalar; } + pub use salvo_craft::*; }