9 个版本 (破坏性更新)
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.2.0 | 2021年7月13日 |
#9 在 #ajars
每月 108 次下载
10KB
170 行
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 的前端(例如 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
要在 Cargo.toml 文件中启用 reqwest
功能,以与 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
要在 Cargo.toml 文件中启用 surf
功能,以与 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
要在 Cargo.toml 文件中启用 actix_web
功能,以与 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
要在 Cargo.toml 文件中启用 axum
功能,以与 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 {}
}
依赖关系
~3–14MB
~212K SLoC