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

MIT 许可证

8KB
137 代码行

AjaRS

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::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前端(例如YewSycamore等...)。

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