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日

#137HTTP服务器 中排名

Download history 133/week @ 2024-04-15 23/week @ 2024-04-22 69/week @ 2024-04-29 99/week @ 2024-05-06 76/week @ 2024-05-13 63/week @ 2024-05-20 56/week @ 2024-05-27 48/week @ 2024-06-03 5/week @ 2024-06-10 15/week @ 2024-06-17 45/week @ 2024-06-24 83/week @ 2024-07-01 5/week @ 2024-07-08 42/week @ 2024-07-22 106/week @ 2024-07-29

每月 153次下载

MIT 许可证

33KB
325 代码行

AjaRS

crates.io Build Status codecov

AjaRS是一个小型Rust库,用于删除在定义服务器端REST端点和调用它的REST客户端之间重复的代码。

问题

当我们创建一个REST端点时,我们需要提供至少四个不同的值

  1. 资源的路径
  2. HTTP方法
  3. 消耗的JSON类型
  4. 产生的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前端中使用(例如 YewSycamore 等...)。

要使用它,在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