Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add salvo-craft-macros, solve the issue #919 #920

Merged
merged 3 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
34 changes: 34 additions & 0 deletions crates/craft-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>"]

[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
66 changes: 66 additions & 0 deletions crates/craft-macros/README.md
Original file line number Diff line number Diff line change
@@ -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<i64, true>, right: QueryParam<i64, true>) -> String {
(self.state + *left + *right).to_string()
}
/// doc line 3
/// doc line 4
#[craft(handler)]
pub(crate) fn add2(
self: ::std::sync::Arc<Self>,
left: QueryParam<i64, true>,
right: QueryParam<i64, true>,
) -> String {
(self.state + *left + *right).to_string()
}
/// doc line 5
/// doc line 6
#[craft(handler)]
pub fn add3(left: QueryParam<i64, true>, right: QueryParam<i64, true>) -> 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.
55 changes: 55 additions & 0 deletions crates/craft-macros/examples/add.rs
Original file line number Diff line number Diff line change
@@ -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<i64, true>, right: QueryParam<i64, true>) -> String {
(self.state + *left + *right).to_string()
}
/// doc line 3
/// doc line 4
#[craft(endpoint)]
pub(crate) fn add2(
self: ::std::sync::Arc<Self>,
left: QueryParam<i64, true>,
right: QueryParam<i64, true>
) -> 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<i64, true>, right: QueryParam<i64, true>) -> String {
(*left + *right).to_string()
}
}
161 changes: 161 additions & 0 deletions crates/craft-macros/src/craft.rs
Original file line number Diff line number Diff line change
@@ -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<TokenStream> {
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<Option<Attribute>> {
let mut index: Option<usize> = None;
let mut new_attr: Option<Attribute> = 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<TokenStream> = 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<Self> {
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<Self>" | "&Arc<Self>" => Ok(Self::ArcSelf),
_ => {
if ty.ends_with("::Arc<Self>") {
Ok(Self::ArcSelf)
} else {
Err(
syn::Error::new_spanned(
method,
"#[craft] method receiver must be '&self', 'Arc<Self>' or '&Arc<Self>'"
)
)
}
}
}
}
}

fn rewrite_method(self_ty: Box<Type>, 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<Self>), 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(())
}
61 changes: 61 additions & 0 deletions crates/craft-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<i64, true>, right: QueryParam<i64, true>) -> String {
/// (self.state + *left + *right).to_string()
/// }
/// /// doc line 3
/// /// doc line 4
/// #[craft(handler)]
/// pub(crate) fn add2(
/// self: ::std::sync::Arc<Self>,
/// left: QueryParam<i64, true>,
/// right: QueryParam<i64, true>,
/// ) -> String {
/// (self.state + *left + *right).to_string()
/// }
/// /// doc line 5
/// /// doc line 6
/// #[craft(handler)]
/// pub fn add3(left: QueryParam<i64, true>, right: QueryParam<i64, true>) -> 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(),
}
}
Loading