diff --git a/Cargo.toml b/Cargo.toml index 865ee38..90a2169 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ i18n = { path = "i18n" } [workspace.dependencies] tokio = "1.36" -axum = "0.7" +axum = "0.7.7" tracing = "0.1" tower-http = "0.5" validator = "0.17" diff --git a/README.MD b/README.MD index c1ba91d..729a8e7 100644 --- a/README.MD +++ b/README.MD @@ -4,4 +4,8 @@ - [*] 参考example中的jwt实现方式,移除context对extension的依赖?那么language-tag该怎么处理? - [ ] 参考rocket,移除参数的元组类型 - [ ] 模拟request_util能否避免涉及到的parts.clone()? -- [ ] 能否自定义实现宏路由 \ No newline at end of file +- [ ] 能否自定义实现宏路由 + + +### note +- 展开宏:cargo expand --manifest-path .\server\Cargo.toml controller::feedback_controller \ No newline at end of file diff --git a/domain/src/vo/account.rs b/domain/src/vo/account.rs index 68f0aec..4e7a0fb 100644 --- a/domain/src/vo/account.rs +++ b/domain/src/vo/account.rs @@ -1,4 +1,4 @@ -use derive::Responsable; +use macros::Responsable; use serde::{Deserialize, Serialize}; use sqlx::types::JsonValue; diff --git a/domain/src/vo/feedback.rs b/domain/src/vo/feedback.rs index 9a77a26..34703a5 100644 --- a/domain/src/vo/feedback.rs +++ b/domain/src/vo/feedback.rs @@ -1,4 +1,4 @@ -use derive::Responsable; +use macros::Responsable; use serde::Serialize; use crate::entities::feedback::Feedback; diff --git a/feeback_controller_ex.rs b/feeback_controller_ex.rs new file mode 100644 index 0000000..e69de29 diff --git a/feedback_controller_e.rs b/feedback_controller_e.rs new file mode 100644 index 0000000..e69de29 diff --git a/library/src/extractor/path_extractor.rs b/library/src/extractor/path_extractor.rs index 6948043..9cd14f2 100644 --- a/library/src/extractor/path_extractor.rs +++ b/library/src/extractor/path_extractor.rs @@ -1,5 +1,3 @@ -use std::fmt::Display; - use axum::{async_trait, extract::{path::ErrorKind, rejection::PathRejection, FromRequestParts}}; use http::request::Parts; use i18n::{message, message_ids::MessageId}; @@ -13,7 +11,7 @@ pub struct PathVar(pub T); #[async_trait] impl FromRequestParts for PathVar where - T: DeserializeOwned + Send + Display, + T: DeserializeOwned + Send, S: Send + Sync, { type Rejection = ResErr; @@ -25,7 +23,6 @@ where match axum::extract::Path::::from_request_parts(parts, state).await { Ok(value) => { - tracing::info!("path params: {}", value.0); Ok(Self(value.0)) }, Err(rejection) => { diff --git a/library/src/typed_router.rs b/library/src/typed_router.rs index fd35d8f..b36f8c1 100644 --- a/library/src/typed_router.rs +++ b/library/src/typed_router.rs @@ -27,7 +27,9 @@ where type State = S; fn typed_route(self, handler: TypedHandler) -> Self { + tracing::info!("start add router"); let (path, method_router) = handler(); + tracing::info!("adding typed route: {} - {:?}", path, method_router); self.route(path, method_router) } } \ No newline at end of file diff --git a/macro/Cargo.toml b/macro/Cargo.toml index eeb47eb..745c196 100644 --- a/macro/Cargo.toml +++ b/macro/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [lib] -name = "derive" +name = "macros" path = "src/lib.rs" proc-macro = true diff --git a/macro/src/lib.rs b/macro/src/lib.rs index c3b1912..b5dc16a 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -4,10 +4,11 @@ extern crate syn; extern crate proc_macro; -use proc_macro::TokenStream; // 用于处理宏输入 -use quote::quote; // 用于生成代码 -use syn::{parse_macro_input, Data, DeriveInput, Fields}; // 用于解析宏输入 +use proc_macro::TokenStream; // 用于生成代码 +use syn::{parse_macro_input, parse_quote, DeriveInput}; // 用于解析宏输入 +mod responsable; +mod route; // 定义一个名为`responsable`的过程宏 // #[proc_macro_derive(Responsable, attributes(status, headers))] @@ -15,128 +16,23 @@ use syn::{parse_macro_input, Data, DeriveInput, Fields}; // 用于解析宏输 pub fn responsable(input: TokenStream) -> TokenStream { // 解析宏输入为`DeriveInput` let input = parse_macro_input!(input as DeriveInput); - let name = &input.ident; // 获取结构体名称 // 根据数据类型生成不同的代码 - let gen = match input.data { - Data::Struct(ref data) => match data.fields { - Fields::Named(ref _fields) => { - // // 处理命名字段 - // let field_names = fields.named.iter().map(|f| &f.ident); // 获取所有字段名称 - // let field_types = fields.named.iter().map(|f| &f.ty); // 获取所有字段类型 + responsable::gen_responsable(input) +} - // // 获取`status`属性 - // let status = match input.attrs.iter().find(|attr| attr.path().is_ident("status")) { - // Some(attr) => { - // let status_lit = attr.parse_args::().unwrap(); // 解析`status`属性值 - // quote! { #status_lit } // 生成状态码 - // }, - // None => quote! { hyper::StatusCode::OK }, // 默认状态码为`OK` - // }; +// #[proc_macro_attribute] +// pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { +// route::gen_route(attr, item) +// // item +// } - // // 获取`headers`属性 - // let headers = match input.attrs.iter().find(|attr| attr.path().is_ident("headers")) { - // Some(attr) => { - // let headers_lit = attr.parse_args::().unwrap(); // 解析`headers`属性值 - // quote! { #headers_lit } // 生成头部信息 - // }, - // None => quote! { hyper::HeaderMap::new() }, // 默认头部信息为空 - // }; +#[proc_macro_attribute] +pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { + route::gen_route(attr, item) +} - // 生成实现`IntoResponse` trait的代码 - quote! { - impl axum::response::IntoResponse for #name { - fn into_response(self) -> axum::http::Response { - let mut builder = hyper::Response::builder(); - // .status(#status) // 设置状态码 - // .headers(#headers); // 设置头部信息 - - let body = serde_json::to_string(&self).expect("Failed to serialize response"); // 序列化结构体为JSON字符串 - - axum::http::Response::new(axum::body::Body::from(body)) // 构建响应 - } - } - } - }, - Fields::Unnamed(ref _fields) => { - // // 处理未命名字段 - // let field_names = fields.unnamed.iter().enumerate().map(|(i, _)| i); // 获取所有字段索引 - // let field_types = fields.unnamed.iter().map(|f| &f.ty); // 获取所有字段类型 - - // // 获取`status`属性 - // let status = match input.attrs.iter().find(|attr| attr.path().is_ident("status")) { - // Some(attr) => { - // let status_lit = attr.parse_args::().unwrap(); // 解析`status`属性值 - // quote! { #status_lit } // 生成状态码 - // }, - // None => quote! { hyper::StatusCode::OK }, // 默认状态码为`OK` - // }; - - // // 获取`headers`属性 - // let headers = match input.attrs.iter().find(|attr| attr.path().is_ident("headers")) { - // Some(attr) => { - // let headers_lit = attr.parse_args::().unwrap(); // 解析`headers`属性值 - // quote! { #headers_lit } // 生成头部信息 - // }, - // None => quote! { hyper::HeaderMap::new() }, // 默认头部信息为空 - // }; - - // 生成实现`IntoResponse` trait的代码 - quote! { - impl axum::response::IntoResponse for #name { - fn into_response(self) -> axum::http::Response { - // let mut builder = hyper::Response::builder(); - // .status(#status) // 设置状态码 - // .headers(#headers); // 设置头部信息 - - let body = serde_json::to_string(&self).expect("Failed to serialize response"); // 序列化结构体为JSON字符串 - - axum::http::Response::new(axum::body::Body::from(body)) // 构建响应 - } - } - } - }, - Fields::Unit => { - // // 处理无字段的结构体 - // // 获取`status`属性 - // let status = match input.attrs.iter().find(|attr| attr.path().is_ident("status")) { - // Some(attr) => { - // let status_lit = attr.parse_args::().unwrap(); // 解析`status`属性值 - // quote! { #status_lit } // 生成状态码 - // }, - // None => quote! { hyper::StatusCode::OK }, // 默认状态码为`OK` - // }; - - // // 获取`headers`属性 - // let headers = match input.attrs.iter().find(|attr| attr.path().is_ident("headers")) { - // Some(attr) => { - // let headers_lit = attr.parse_args::().unwrap(); // 解析`headers`属性值 - // quote! { #headers_lit } // 生成头部信息 - // }, - // None => quote! { hyper::HeaderMap::new() }, // 默认头部信息为空 - // }; - - // 生成实现`IntoResponse` trait的代码 - quote! { - impl axum::response::IntoResponse for #name { - fn into_response(self) -> axum::http::Response { - // let mut builder = hyper::Response::builder(); - // .status(#status) // 设置状态码 - // .headers(#headers); // 设置头部信息 - - let body = serde_json::to_string(&self).expect("Failed to serialize response"); // 序列化结构体为JSON字符串 - - axum::http::Response::new(hyper::body::Body::from(body)) // 构建响应 - } - } - } - }, - }, - Data::Enum(_) | Data::Union(_) => { - // 不支持枚举或联合体 - unimplemented!("IntoResponse derive is not implemented for enums or unions.") - }, - }; - - gen.into() // 将生成的代码转换为`TokenStream` +#[proc_macro_attribute] +pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream { + route::gen_get_route(attr, item) } \ No newline at end of file diff --git a/macro/src/responsable.rs b/macro/src/responsable.rs new file mode 100644 index 0000000..78d7426 --- /dev/null +++ b/macro/src/responsable.rs @@ -0,0 +1,133 @@ +extern crate proc_macro2; +extern crate quote; +extern crate syn; + +extern crate proc_macro; + +use proc_macro::TokenStream; // 用于处理宏输入 +use quote::quote; // 用于生成代码 +use syn::{Data, DeriveInput, Fields}; // 用于解析宏输入 + +pub fn gen_responsable(input: DeriveInput) -> TokenStream { + let name = &input.ident; // 获取结构体名称 + let gen = match input.data { + Data::Struct(ref data) => match data.fields { + Fields::Named(ref _fields) => { + // // 处理命名字段 + // let field_names = fields.named.iter().map(|f| &f.ident); // 获取所有字段名称 + // let field_types = fields.named.iter().map(|f| &f.ty); // 获取所有字段类型 + + // // 获取`status`属性 + // let status = match input.attrs.iter().find(|attr| attr.path().is_ident("status")) { + // Some(attr) => { + // let status_lit = attr.parse_args::().unwrap(); // 解析`status`属性值 + // quote! { #status_lit } // 生成状态码 + // }, + // None => quote! { hyper::StatusCode::OK }, // 默认状态码为`OK` + // }; + + // // 获取`headers`属性 + // let headers = match input.attrs.iter().find(|attr| attr.path().is_ident("headers")) { + // Some(attr) => { + // let headers_lit = attr.parse_args::().unwrap(); // 解析`headers`属性值 + // quote! { #headers_lit } // 生成头部信息 + // }, + // None => quote! { hyper::HeaderMap::new() }, // 默认头部信息为空 + // }; + + // 生成实现`IntoResponse` trait的代码 + quote! { + impl axum::response::IntoResponse for #name { + fn into_response(self) -> axum::http::Response { + let mut builder = hyper::Response::builder(); + // .status(#status) // 设置状态码 + // .headers(#headers); // 设置头部信息 + + let body = serde_json::to_string(&self).expect("Failed to serialize response"); // 序列化结构体为JSON字符串 + + axum::http::Response::new(axum::body::Body::from(body)) // 构建响应 + } + } + } + }, + Fields::Unnamed(ref _fields) => { + // // 处理未命名字段 + // let field_names = fields.unnamed.iter().enumerate().map(|(i, _)| i); // 获取所有字段索引 + // let field_types = fields.unnamed.iter().map(|f| &f.ty); // 获取所有字段类型 + + // // 获取`status`属性 + // let status = match input.attrs.iter().find(|attr| attr.path().is_ident("status")) { + // Some(attr) => { + // let status_lit = attr.parse_args::().unwrap(); // 解析`status`属性值 + // quote! { #status_lit } // 生成状态码 + // }, + // None => quote! { hyper::StatusCode::OK }, // 默认状态码为`OK` + // }; + + // // 获取`headers`属性 + // let headers = match input.attrs.iter().find(|attr| attr.path().is_ident("headers")) { + // Some(attr) => { + // let headers_lit = attr.parse_args::().unwrap(); // 解析`headers`属性值 + // quote! { #headers_lit } // 生成头部信息 + // }, + // None => quote! { hyper::HeaderMap::new() }, // 默认头部信息为空 + // }; + + // 生成实现`IntoResponse` trait的代码 + quote! { + impl axum::response::IntoResponse for #name { + fn into_response(self) -> axum::http::Response { + // let mut builder = hyper::Response::builder(); + // .status(#status) // 设置状态码 + // .headers(#headers); // 设置头部信息 + + let body = serde_json::to_string(&self).expect("Failed to serialize response"); // 序列化结构体为JSON字符串 + + axum::http::Response::new(axum::body::Body::from(body)) // 构建响应 + } + } + } + }, + Fields::Unit => { + // // 处理无字段的结构体 + // // 获取`status`属性 + // let status = match input.attrs.iter().find(|attr| attr.path().is_ident("status")) { + // Some(attr) => { + // let status_lit = attr.parse_args::().unwrap(); // 解析`status`属性值 + // quote! { #status_lit } // 生成状态码 + // }, + // None => quote! { hyper::StatusCode::OK }, // 默认状态码为`OK` + // }; + + // // 获取`headers`属性 + // let headers = match input.attrs.iter().find(|attr| attr.path().is_ident("headers")) { + // Some(attr) => { + // let headers_lit = attr.parse_args::().unwrap(); // 解析`headers`属性值 + // quote! { #headers_lit } // 生成头部信息 + // }, + // None => quote! { hyper::HeaderMap::new() }, // 默认头部信息为空 + // }; + + // 生成实现`IntoResponse` trait的代码 + quote! { + impl axum::response::IntoResponse for #name { + fn into_response(self) -> axum::http::Response { + // let mut builder = hyper::Response::builder(); + // .status(#status) // 设置状态码 + // .headers(#headers); // 设置头部信息 + + let body = serde_json::to_string(&self).expect("Failed to serialize response"); // 序列化结构体为JSON字符串 + + axum::http::Response::new(hyper::body::Body::from(body)) // 构建响应 + } + } + } + }, + }, + Data::Enum(_) | Data::Union(_) => { + // 不支持枚举或联合体 + unimplemented!("IntoResponse derive is not implemented for enums or unions.") + }, + }; + gen.into() +} \ No newline at end of file diff --git a/macro/src/route.rs b/macro/src/route.rs new file mode 100644 index 0000000..0bfa007 --- /dev/null +++ b/macro/src/route.rs @@ -0,0 +1,168 @@ +extern crate proc_macro2; +extern crate quote; +extern crate syn; + +extern crate proc_macro; + +use parse::{Parse, ParseStream}; +use proc_macro::{Span, TokenStream}; use punctuated::Punctuated; +// 用于处理宏输入 +use quote::quote; // 用于生成代码 +use syn::*; + +struct Args { + vars: Vec +} + +impl Parse for Args { + fn parse(input: ParseStream) -> syn::parse::Result { + let vars = Punctuated::::parse_terminated(input)?; + + Ok(Args { + vars: vars.into_iter().collect(), + }) + } +} + +impl Args { + pub fn get_method(&self) -> syn::Result { + match self.vars.get(0) { + Some(var) => Ok(var.clone()), + None => return Err(syn::Error::new( + Span::call_site().into(), + "No Method was provided" + )) + } + } + + pub fn get_route(&self) -> syn::Result { + match self.vars.get(0) { + Some(var) => Ok(var.clone()), + None => return Err(syn::Error::new( + Span::call_site().into(), + "No Route was provided" + )) + } + } +} + +pub fn gen_route(attr: TokenStream, item: TokenStream) -> TokenStream { + let args = parse_macro_input!(attr as Args); + let func = parse_macro_input!(item as ItemFn); + + let vis = func.vis.clone(); + let ident = func.sig.ident.clone(); + + let method = args.get_method().unwrap(); + let route = args.get_route().unwrap(); + + let expanded = quote! { + #[allow(non_camel_case_types)] + #vis struct #ident; + + impl #ident { + #vis fn route() -> axum::Router { + #func + + axum::Router::new().route(#route, #method (#ident)) + } + } + }; + + expanded.into() +} + +pub fn gen_get_route(attr: TokenStream, item: TokenStream) -> TokenStream { + let args = parse_macro_input!(attr as Args); + let func = parse_macro_input!(item as ItemFn); + + let vis = func.vis.clone(); + let ident = func.sig.ident.clone(); + + let route = args.get_route().unwrap(); + + let expanded = quote! { + #vis fn #ident () -> (&'static str, axum::routing::method_routing::MethodRouter) { + #func + + (#route, axum::routing::get(#ident)) + } + }; + expanded.into() +} + +pub fn gen_post_route(attr: TokenStream, item: TokenStream) -> TokenStream { + let args = parse_macro_input!(attr as Args); + let func = parse_macro_input!(item as ItemFn); + + let vis = func.vis.clone(); + let ident = func.sig.ident.clone(); + + let route = args.get_route().unwrap(); + + let expanded = quote! { + #vis fn #ident () -> (&'static str, axum::routing::method_routing::MethodRouter) { + #func + + (#route, axum::routing::post(#ident)) + } + }; + expanded.into() +} + +pub fn gen_delete_route(attr: TokenStream, item: TokenStream) -> TokenStream { + let args = parse_macro_input!(attr as Args); + let func = parse_macro_input!(item as ItemFn); + + let vis = func.vis.clone(); + let ident = func.sig.ident.clone(); + + let route = args.get_route().unwrap(); + + let expanded = quote! { + #vis fn #ident () -> (&'static str, axum::routing::method_routing::MethodRouter) { + #func + + (#route, axum::routing::delete(#ident)) + } + }; + expanded.into() +} + +pub fn gen_put_route(attr: TokenStream, item: TokenStream) -> TokenStream { + let args = parse_macro_input!(attr as Args); + let func = parse_macro_input!(item as ItemFn); + + let vis = func.vis.clone(); + let ident = func.sig.ident.clone(); + + let route = args.get_route().unwrap(); + + let expanded = quote! { + #vis fn #ident () -> (&'static str, axum::routing::method_routing::MethodRouter) { + #func + + (#route, axum::routing::put(#ident)) + } + }; + expanded.into() +} + +pub fn gen_option_route(attr: TokenStream, item: TokenStream) -> TokenStream { + let args = parse_macro_input!(attr as Args); + let func = parse_macro_input!(item as ItemFn); + + let vis = func.vis.clone(); + let ident = func.sig.ident.clone(); + + let route = args.get_route().unwrap(); + + let expanded = quote! { + #vis fn #ident () -> (&'static str, axum::routing::method_routing::MethodRouter) { + #func + + (#route, axum::routing::option(#ident)) + } + }; + expanded.into() +} diff --git a/server/src/controller/feedback_controller.rs b/server/src/controller/feedback_controller.rs index 173168d..e75e2e7 100644 --- a/server/src/controller/feedback_controller.rs +++ b/server/src/controller/feedback_controller.rs @@ -1,10 +1,14 @@ +extern crate macros; + use domain::dto::feedback::FeedbackAdd; use domain::dto::pageable::PageParams; use domain::vo::feedback::FeedbackPageable; use library::context::Context; +use library::extractor::path_extractor::PathVar; use library::model::response::ResResult; use library::extractor::body_extractor::JsonBody; use library::extractor::query_extractor::QueryParams; +use macros::get; use crate::service; @@ -31,4 +35,18 @@ pub async fn get_feedback_list_by_page( page_params.page_size.unwrap(), ) .await +} + +// #[route(get, "/feedback/:page/:pageSize")] +#[get("/feedback/:page/:pageSize")] +pub async fn get_feedback_list( + context: Context, + PathVar(page_params): PathVar, +) -> ResResult { + service::feedback_service::get_feedback_list_by_page( + context, + page_params.page.unwrap(), + page_params.page_size.unwrap(), + ) + .await } \ No newline at end of file diff --git a/server/src/controller/mod.rs b/server/src/controller/mod.rs index 6fc7a11..753ff0d 100644 --- a/server/src/controller/mod.rs +++ b/server/src/controller/mod.rs @@ -1,10 +1,16 @@ -use axum::{routing::post, Router}; +use axum::{routing::{get, post, MethodRouter}, Router}; +use library::typed_router::TypedRouter; pub mod account_controller; pub mod feedback_controller; pub fn init() -> Router { - Router::new() + // let a = feedback_controller::get_feedback_list; + + // axum::routing::method_routing::MethodRouter::new() + + + let router = Router::new() .route( "/account/google", post(account_controller::authenticate_google), @@ -22,4 +28,17 @@ pub fn init() -> Router { post(feedback_controller::add_feedback) .get(feedback_controller::get_feedback_list_by_page) ) -} \ No newline at end of file + .typed_route(feedback_controller::get_feedback_list) + // .merge(feedback_controller::get_feedback_list::route()) + // .typed_route(route) + // .route( + // "/feedback/:page/:pageSize", + // get(feedback_controller::get_feedback_list) + // ) + ; + return router; +} + +// fn route() -> (&'static str, MethodRouter) { +// ("/feedback", axum::routing::get(feedback_controller::get_feedback_list_by_page)) +// } \ No newline at end of file