8个版本 (5个破坏性更改)
0.8.0 | 2022年4月1日 |
---|---|
0.7.0 | 2021年12月3日 |
0.6.3 | 2021年8月27日 |
0.5.0 | 2021年8月24日 |
0.3.0 | 2021年7月16日 |
在 HTTP客户端 中排名468
8KB
137 代码行
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::ActixWebHandler;
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::{AjarsReqwest, reqwest::ClientBuilder};
let ajars = AjarsReqwest::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::AjarsWeb;
use ajars::Rest;
use serde::{Deserialize, Serialize};
pub const PING: Rest<PingRequest, PingResponse> = Rest::post("/ping");
async fn client() {
let ajars = AjarsWeb::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::{AjarsReqwest, reqwest::ClientBuilder};
use serde::{Deserialize, Serialize};
pub const PING: Rest<PingRequest, PingResponse> = Rest::post("/ping");
async fn client() {
let ajars = AjarsReqwest::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::AjarsSurf;
use serde::{Deserialize, Serialize};
pub const PING: Rest<PingRequest, PingResponse> = Rest::post("/ping");
async fn client() {
let ajars = AjarsSurf::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::ActixWebHandler;
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::{boxed, Body, BoxBody, HttpBody}, http::Response, response::IntoResponse, Router};
use ajars::axum::AxumHandler;
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use derive_more::{Display, Error};
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);
ajars::axum::axum::Server::bind(&addr).serve(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<BoxBody> {
Response::new(boxed(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 {}
}
依赖项
~6.5–9.5MB
~198K SLoC