9次重大版本更新
0.11.0 | 2024年4月1日 |
---|---|
0.10.0 | 2023年11月28日 |
0.8.0 | 2022年4月1日 |
0.7.0 | 2021年12月3日 |
0.4.0 | 2021年7月18日 |
#137 在 HTTP服务器 中排名
每月 153次下载
33KB
325 代码行
AjaRS
AjaRS是一个小型Rust库,用于删除在定义服务器端REST端点和调用它的REST客户端之间重复的代码。
问题
当我们创建一个REST端点时,我们需要提供至少四个不同的值
- 资源的路径
- HTTP方法
- 消耗的JSON类型
- 产生的JSON类型
创建该端点的REST客户端时必须提供完全相同的四个值。
例如,如果我们使用 actix-web,可以使用以下方式创建端点
#[cfg(all(feature = "actix_web", feature = "reqwest"))]
mod without_ajars {
use ajars::actix_web::actix_web::{App, Result, web::{self, Json}};
use serde::{Deserialize, Serialize};
fn server() {
App::new().service(
web::resource("/ping") // PATH definition here
.route(web::post() // HTTP Method definition here
.to(ping) // The signature of the `ping` fn determines the
// JSON types produced and consumed. In this case
// PingRequest and PingResponse
)
);
async fn ping(_body: Json<PingRequest>) -> Result<Json<PingResponse>> {
Ok(Json(PingResponse {}))
}
}
// Let's now declare a client using [reqwest](https://github.com/seanmonstar/reqwest)
pub async fn client() {
use ajars::reqwest::reqwest::ClientBuilder;
let client = ClientBuilder::new().build().unwrap();
let url = "http://127.0.0.1:8080/ping"; // Duplicated '/ping' path definition
client.post(url) // Duplicated HTTP Post method definition
.json(&PingRequest {}) // Duplicated request type. Not checked at compile time
.send().await.unwrap()
.json::<PingResponse>().await.unwrap(); // Duplicated response type. Not checked at compile time
}
#[derive(Serialize, Deserialize, Debug)]
pub struct PingRequest {}
#[derive(Serialize, Deserialize, Debug)]
pub struct PingResponse {}
}
这些值是否可以只声明一次,同时所有类型都在编译时检查呢?
AjaRs解决方案
Ajars允许客户端和服务器使用单个定义。这消除了代码重复,同时允许在编译时验证请求和响应类型是否正确。
现在让我们使用Ajars重新定义之前的端点:之前端点的定义
#[cfg(all(feature = "actix_web", feature = "reqwest"))]
mod with_ajars {
use ajars::Rest;
use serde::{Deserialize, Serialize};
// This defines a 'POST' call with path /ping, request type 'PingRequest' and response type 'PingResponse'
// This should ideally be declared in a commond library imported by both the server and the client
pub const PING: Rest<PingRequest, PingResponse> = Rest::post("/ping");
// The the server side endpoint creation now becomes:
fn server() {
use ajars::actix_web::AjarsServerActixWebHandler;
use ajars::{actix_web::actix_web::{App, HttpServer, ResponseError}};
use derive_more::{Display, Error};
HttpServer::new(move ||
App::new().service(
PING.to(ping) // here Ajarj takes care of the endpoint creation
)
);
#[derive(Debug, Display, Error)]
enum UserError {
#[display(fmt = "Validation error on field: {}", field)]
ValidationError { field: String },
}
impl ResponseError for UserError {}
async fn ping(_body: PingRequest) -> Result<PingResponse, UserError> {
Ok(PingResponse {})
}
// start the server...
}
// The client, using reqwest, becomes:
async fn client() {
use ajars::reqwest::{AjarsClientReqwest, reqwest::ClientBuilder};
let ajars = AjarsClientReqwest::new(ClientBuilder::new().build().unwrap(), "http://127.0.0.1:8080");
// Performs a POST request to http://127.0.0.1:8080/ping
// The PingRequest and PingResponse types are enforced at compile time
let response = ajars
.request(&PING)
.send(&PingRequest {})
.await
.unwrap();
}
#[derive(Serialize, Deserialize, Debug)]
pub struct PingRequest {}
#[derive(Serialize, Deserialize, Debug)]
pub struct PingResponse {}
}
支持的客户端
浏览器中的WASM (web-sys)
Ajars提供了一个基于 web-sys 的轻量级客户端实现,这将在浏览器中运行的基于WASM的Web前端中使用(例如 Yew, Sycamore 等...)。
要使用它,在Cargo.toml文件中启用 web
功能
ajars = { version = "LAST_VERSION", features = ["web"] }
示例
#[cfg(feature = "web")]
mod web {
use ajars::web::AjarsClientWeb;
use ajars::Rest;
use serde::{Deserialize, Serialize};
pub const PING: Rest<PingRequest, PingResponse> = Rest::post("/ping");
async fn client() {
let ajars = AjarsClientWeb::new("").expect("Should build Ajars");
let response = ajars
.request(&PING) // <-- Here's everything required
.send(&PingRequest {})
.await
.unwrap();
}
#[derive(Serialize, Deserialize, Debug)]
pub struct PingRequest {}
#[derive(Serialize, Deserialize, Debug)]
pub struct PingResponse {}
}
Reqwest
要与其一起使用 reqwest,在Cargo.toml文件中启用 reqwest
功能
ajars = { version = "LAST_VERSION", features = ["reqwest"] }
示例
#[cfg(feature = "reqwest")]
mod reqwest {
use ajars::Rest;
use ajars::reqwest::{AjarsClientReqwest, reqwest::ClientBuilder};
use serde::{Deserialize, Serialize};
pub const PING: Rest<PingRequest, PingResponse> = Rest::post("/ping");
async fn client() {
let ajars = AjarsClientReqwest::new(ClientBuilder::new().build().unwrap(), "http://127.0.0.1:8080");
let response = ajars
.request(&PING) // <-- Here's everything required
.send(&PingRequest {})
.await
.unwrap();
}
#[derive(Serialize, Deserialize, Debug)]
pub struct PingRequest {}
#[derive(Serialize, Deserialize, Debug)]
pub struct PingResponse {}
}
Surf
要与其一起使用 surf,在Cargo.toml文件中启用 surf
功能
ajars = { version = "LAST_VERSION", features = ["surf"] }
示例
#[cfg(feature = "surf")]
mod surf {
use ajars::Rest;
use ajars::surf::AjarsClientSurf;
use serde::{Deserialize, Serialize};
pub const PING: Rest<PingRequest, PingResponse> = Rest::post("/ping");
async fn client() {
let ajars = AjarsClientSurf::new(ajars::surf::surf::client(), "http://127.0.0.1:8080");
let response = ajars
.request(&PING) // <-- Here's everything required
.send(&PingRequest { })
.await
.unwrap();
}
#[derive(Serialize, Deserialize, Debug)]
pub struct PingRequest {}
#[derive(Serialize, Deserialize, Debug)]
pub struct PingResponse {}
}
支持的服务器
Actix-web
与 actix-web 一起使用时,请启用 Cargo.toml 文件中的 actix_web
功能。
ajars = { version = "LAST_VERSION", features = ["actix_web"] }
示例
#[cfg(feature = "actix_web")]
mod actix_web {
use ajars::Rest;
use serde::{Deserialize, Serialize};
use ajars::actix_web::AjarsServerActixWebHandler;
use ajars::actix_web::actix_web::{App, HttpServer, ResponseError};
use derive_more::{Display, Error};
pub const PING: Rest<PingRequest, PingResponse> = Rest::post("/ping");
async fn server() {
HttpServer::new(move ||
App::new().service(
PING.to(ping) // <-- Here's everything required
)
)
.bind("127.0.0.1:8080")
.unwrap()
.run()
.await
.unwrap();
}
#[derive(Debug, Display, Error)]
enum UserError {
#[display(fmt = "Validation error on field: {}", field)]
ValidationError { field: String },
}
impl ResponseError for UserError {}
async fn ping(_body: PingRequest) -> Result<PingResponse, UserError> {
Ok(PingResponse {})
}
#[derive(Serialize, Deserialize, Debug)]
pub struct PingRequest {}
#[derive(Serialize, Deserialize, Debug)]
pub struct PingResponse {}
}
Axum
与 axum 一起使用时,请启用 Cargo.toml 文件中的 axum
功能。
ajars = { version = "LAST_VERSION", features = ["axum"] }
示例
#[cfg(feature = "axum")]
mod axum {
use ajars::Rest;
use ajars::axum::axum::{body::{Body, HttpBody}, http::Response, response::IntoResponse, Router};
use ajars::axum::AjarsServerAxumHandler;
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use derive_more::{Display, Error};
use tokio::net::TcpListener;
pub const PING: Rest<PingRequest, PingResponse> = Rest::post("/ping");
async fn server() {
let app = Router::new()
.merge(PING.to(ping)); // <-- Here's everything required
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
println!("Start axum to {}", addr);
let listener = TcpListener::bind(&addr).await.unwrap();
ajars::axum::axum::serve(listener, app.into_make_service()).await.unwrap();
}
#[derive(Debug, Display, Error)]
enum UserError {
#[display(fmt = "Validation error on field: {}", field)]
ValidationError { field: String },
}
impl IntoResponse for UserError {
fn into_response(self) -> Response<Body> {
Response::new(Body::empty())
}
}
async fn ping(_body: PingRequest) -> Result<PingResponse, UserError> {
Ok(PingResponse {})
}
#[derive(Serialize, Deserialize, Debug)]
pub struct PingRequest {}
#[derive(Serialize, Deserialize, Debug)]
pub struct PingResponse {}
}
依赖项
~0.1–12MB
~153K SLoC