7 个版本 (4 个破坏性更新)

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.4.0 2021 年 7 月 18 日

#745 in WebAssembly

每月 21 次下载

MIT 许可证

18KB
351

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

要在 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

要在 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

要在 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 {}
}

依赖项

~7–9.5MB
~181K SLoC