Smithy 结合 Axum 实战:业务逻辑对接与用户鉴权机制
通过编写 .smithy 接口定义语言(IDL)文件,smithy-rs 工具链能够为我们生成类型安全的 Axum 服务端骨架。该骨架包含了数据模型、错误枚举、业务接口 Trait 以及可运行的 Axum Router。然而,生成的 Trait 方法默认均为 unimplemented!()。接下来的核心任务是赋予这个骨架实际的业务能力:将底层 Service 和 Repository 的逻辑接入生成的接口,并整合身份验证功能。
1. 实现自动生成的 Trait 接口
smithy-rs 产出的核心接口(例如 chat_server::server::ChatService)需要我们提供具体实现。我们将创建一个名为 ChatApiHandler 的结构体来承载这些逻辑。
use crate::ServerContext;
use chat_server::{
error::{ChatServiceError, ListConversationsError, GetConversationError, NotFoundError},
input::{GetConversationInput, ListConversationsInput},
model::Conversation,
output::ListConversationsOutput,
server::ChatService,
};
use async_trait::async_trait;
#[derive(Debug, Clone)]
pub struct ChatApiHandler {
ctx: ServerContext,
}
impl ChatApiHandler {
pub fn with_context(ctx: ServerContext) -> Self {
Self { ctx }
}
}
#[async_trait]
impl ChatService for ChatApiHandler {
async fn list_conversations(
&self,
req: ListConversationsInput,
) -> Result {
tracing::info!("Fetching conversation list: {:?}", req);
// 构造模拟响应数据
let mock_data = vec![
Conversation::builder().id("c-101").user_id(42).title("Project Sync").build().unwrap(),
Conversation::builder().id("c-102").user_id(42).title("Weekly Review").build().unwrap(),
];
Ok(ListConversationsOutput::builder()
.set_conversations(Some(mock_data))
.build()
.unwrap())
}
async fn get_conversation(
&self,
req: GetConversationInput,
) -> Result {
let target_id = req.id();
tracing::info!("Retrieving conversation details for ID: {}", target_id);
if target_id == "not-found" {
let err = NotFoundError::builder()
.message("The requested conversation does not exist.")
.build();
return Err(ChatServiceError::from(GetConversationError::NotFoundError(err)));
}
Ok(Conversation::builder()
.id(target_id)
.user_id(42)
.title("Detailed Discussion")
.build()
.unwrap())
}
}
在错误处理方面,当发生异常时,需将具体的错误结构体(如 NotFoundError)逐层包装为操作级错误(GetConversationError),最终转换为服务级错误枚举(ChatServiceError),以保证严格的类型安全。
2. 状态注入与依赖管理
ChatApiHandler 需要访问数据库连接池和内部服务,这些通常封装在 ServerContext 中。我们可以借助 Axum 的 Extension 中间件将上下文注入到请求生命周期中。
use chat_server::server::ChatServiceServer;
use std::net::SocketAddr;
use tower::ServiceBuilder;
use axum::Extension;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 初始化包含数据库池和内部服务的上下文
let app_ctx = ServerContext::init().await?;
// 实例化 API 处理器
let api_handler = ChatApiHandler::with_context(app_ctx.clone());
// 构建 Axum 路由并注入上下文
let router = ChatServiceServer::new(api_handler)
.layer(
ServiceBuilder::new()
.layer(Extension(app_ctx))
);
let bind_addr: SocketAddr = "127.0.0.1:8080".parse()?;
tracing::info!("Server started on {}", bind_addr);
axum::Server::bind(&bind_addr)
.serve(router.into_make_service())
.await?;
Ok(())
}
smithy-rs 在底层为生成的服务器实现了 FromRequestParts,能够自动从请求的 extensions 中提取所需的状态,使得依赖注入过程对开发者几乎透明。
3. 桥接核心业务层
接下来,将生成的接口与实际的内部业务服务(如 ChatDomainService)对接,完成模型转换与错误映射。
use crate::domain::ChatDomainService;
#[async_trait]
impl ChatService for ChatApiHandler {
async fn list_conversations(
&self,
req: ListConversationsInput,
) -> Result {
let uid = 42; // 假设从鉴权上下文中获取
// 调用领域服务获取内部模型
let domain_models = self.ctx.chat_service
.fetch_user_chats(uid)
.await
.map_err(translate_domain_error)?;
// 将内部模型映射为 Smithy 生成的 API 模型
let api_models: Vec<Conversation> = domain_models
.into_iter()
.map(|m| Conversation::builder()
.id(m.uuid.to_string())
.user_id(m.owner_id)
.title(m.subject)
.build()
.unwrap())
.collect();
Ok(ListConversationsOutput::builder()
.set_conversations(Some(api_models))
.build()
.unwrap())
}
}
fn translate_domain_error(err: anyhow::Error) -> ChatServiceError {
// 实际项目中可根据 err 的具体类型进行精细化匹配
ChatServiceError::InternalServerError(
chat_server::error::InternalServerError::builder()
.message(format!("Internal failure: {}", err))
.build()
)
}
这种设计实现了内部领域模型与外部 API 契约的彻底解耦。虽然增加了模型转换的代码量,但确保了 API 演进不会直接影响核心业务逻辑,同时通过 translate_domain_error 实现了内部异常到 API 标准错误的精确映射。
4. 集成用户鉴权机制
为了保护接口,我们需要引入身份验证。虽然 Smithy 提供了 @auth 注解来声明认证方案,但在 Axum 生态中,结合 Tower 中间件和请求扩展(Extensions)往往更加灵活且易于调试。
通过在 ChatApiHandler 的方法签名中直接引入 Axum 的提取器,可以优雅地获取鉴权信息:
use axum::Extension;
use crate::auth::AuthenticatedUser;
#[async_trait]
impl ChatService for ChatApiHandler {
async fn list_conversations(
&self,
Extension(current_user): Extension<AuthenticatedUser>,
req: ListConversationsInput,
) -> Result {
// 使用从 Token 中解析出的真实用户 ID
let domain_models = self.ctx.chat_service
.fetch_user_chats(current_user.uid)
.await
.map_err(translate_domain_error)?;
// 后续模型转换与响应构建逻辑与前述一致
let api_models: Vec<Conversation> = domain_models
.into_iter()
.map(|m| Conversation::builder()
.id(m.uuid.to_string())
.user_id(m.owner_id)
.title(m.subject)
.build()
.unwrap())
.collect();
Ok(ListConversationsOutput::builder()
.set_conversations(Some(api_models))
.build()
.unwrap())
}
}
现代版本的 smithy-rs 深度兼容 Axum 的提取器机制。只需在 Trait 方法的参数列表中前置 Extension 提取器,代码生成器便会自动处理 HTTP 请求头解析与上下文注入。这使得鉴权逻辑与业务代码完美融合,无需在路由层编写冗余的拦截逻辑,同时保持了处理函数签名的清晰与类型安全。