#open-api #rest #oas #rest-client #response-body

nightly tapioca-codegen

使用OpenAPI规范的类型安全REST客户端

2个版本

使用旧的Rust 2015

0.0.1 2017年6月18日
0.0.0 2017年6月8日

#27 in #response-body


tapioca 中使用

自定义许可

42KB
1K SLoC

Tapioca

Typed APIOllie Coshed into an Acronym)

Crate Build Status

tapioca 是一个用于 rust 的HTTP客户端,旨在帮助编译器帮助 以类型更安全的方式访问REST+JSON API。

它使用 OpenAPI联盟的方案规范 来推断路径和查询参数、请求和响应体等的类型,然后使用 serde 对其进行反序列化。

infer_api!(service, "https://service.api/schema.yml")
use service::path;

fn main() {
    let auth = service::ServerAuth::new();

    match path::get(&auth) {
        Ok(response) => match response.body() {
            path::OkBody::Status200(body) => println!("Thing is: {}", body.thing),
            path::OkBody::UnspecifiedCode(body) => {
                // We're forced to handle every status code in the schema;
                //  including the possibility that the server replies off-script.
                println!("I don't know what thing is!")
            },
        },
        Err(response) => match response.body() {
            path::ErrBody::Status403(body) => println!("That's not my thing"),
            path::ErrBody::UnspecifiedCode(_)
            | path::ErrBody::MalformedJson(_)
            | path::ErrBody::NetworkFailure() => println!("Something went wrong"),
        },
    }
}

因此,我们可以通过状态码对响应进行模式匹配,并将JSON响应作为 rust 类型访问。

tapioca 还旨在防止你因为无效的 序列 请求(例如在特定资源上执行 GET 后跟 DELETE)而伤害自己:这是通过仅从响应和静态值构造资源ID来实现的。 DELETE 函数会导致资源ID参数被 移动(而其他方法只 借用),从而防止其进一步使用。

入门指南

为了开始在项目中使用 tapioca,第一步是找到你希望使用的API的OAS方案。假设它位于 https://example.org/schema.yml。然后,将其最新版本添加到你的 Cargo.toml 中,就像平常一样,并使用宏导入 tapioca

#[macro_use]
extern crate tapioca;

并调用 infer_api 宏来为API构建客户端

infer_api!(example, "https://example.org/schema.yml");

宏在编译时展开,就地构建一个类型化客户端;(几乎)它生成的所有代码都将位于名为 example 的模块下,或者我们在第一个参数中指定的任何名称。唯一的例外是两个crate(必须在根级别加载),为了使用它们的宏,必须在您的crate内部将它们外部化——至少目前,Rust的宏系统正在经历很多变化,这可能会得到改善。这些是 serde_derivetapicoa_codegen;因此,它们也需要在您的 Cargo.toml 中,但任何其他由tapicoa使用的crate(不是用于宏的)将 不需要 这种处理,也不会污染您项目的命名空间。

访问客户端

infer_api 构建的模块包含每个API上可用路径的模块名称,每个模块包含一个适用于该资源的每个有效HTTP方法的功能。例如,要执行 GET /foobars,函数标识符是

example::foobars::get

为了调用此函数,我们可能需要提供一些用于 认证查询参数请求体 等。的参数——这些参数的类型位于与函数同名的模块中,例如

example::foobars::get::QueryParams

认证

在我们发出请求之前,我们需要引入认证——目前,即使是null,也必须为每个请求指定认证。

OAS模式中的认证要求在两个级别上指定:全局服务器和特定操作——《tt class="src-rs">GET /foobars 可以有不同的要求,而其他操作可能只是继承自服务器要求。因此,我们有两种可接受的认证方案 enum

example::ServerAuth
example::foobars::get::OpAuth

取决于操作 examples::foobars::get 是否覆盖了全局认证要求——但类型检查器会告诉我们是否出错。

如果根本不需要认证,我们可以直接使用

example::ServerAuth::new();

如果是HTTP基本认证,那么(根据它是服务器还是操作要求)

example::ServerAuth::Basic(username: String, password:String);
example::foobars::get::OpAuth::Basic(username: String, password:String);

如果是自定义头

example::ServerAuth::ApiKey(api_key: String);
example::foobars::get::OpAuth::ApiKey(api_key: String);

请注意,变体标识符,例如 BasicApiKey,取决于在OAS模式中使用的名称。这是因为可能存在同一类型的多个定义。

发出请求

现在我们已经看到如何构建认证参数,我们可以实际地 GET 一些 foobars

let auth = examples::ServerAuth::new();
let response = examples::foobars::get(&auth);

response 实际上是 Result<Response, Response>:如果响应状态码是错误,我们得到一个 Err(response),否则它是 Ok(response)。这意味着我们可以使用 response.is_okresponse.is_err 和模式匹配

match examples::foobars::get(&auth) {
    Ok(response) => foobar_handler(response),
    Err(response) => err_handler(response),
}

我们可以在每个这些处理程序中使用进一步的模式匹配,以对不同的状态码做出不同的响应

fn foobar_handler(response: Response) {
    match response.body {
        OkBody::Status200(body) => {
            for foobar in body.the_foobars {
                println!("Foobar {} is named {}", foobar.id, foobar.name);
            }
        },
        OkBody::UnspecifiedCode(body)
        | OkBody::MalformedJson(body) => something_else(),
    }
}

其中我们始终有 UnspecifiedCode(不在模式中)和 MalformedJson(无效JSON,或未匹配模式)以及模式中每个可能性的 StatusXXXerr_handler 将类似,带有 ErrBody::Status403 等。

请求体

这个 example::foobars 集合也支持 POST 新的 foobar,我们可以提供如下请求体来创建一个:

let body = example::foobars::post::RequestBody {
    name: "Foobarry".into(),
    age: 12,
    email: None,
};

请求体的结构和字段类型完全由模式定义,可能包括:

  • i32i64
  • bool
  • String
  • Option<_>
  • Vec<_>
  • 更进一步的 struct

查询参数

查询参数的提供方式与请求体类似,如 请求体

let query = example::foobars::get::QueryParams {
    age: 34,
};

路径参数

路径参数稍有不同。由于需要区分 example::foobars::get 与该集合中单个资源上的 GET,路径参数的名称编码在路径模块名称中,例如

example::foobars__id_::get

如果API连续指定了两个资源标识符,这将变成 foobars__id1___id2_。这看起来会很丑,可能在未来的版本中更改。

路径参数可以从响应中构建,例如在创建新资源时,服务器生成了其ID,或者从静态引用中

static provisioned_id = "fea3c8e91baa1";

fn main() {
    let auth = example::ServerAuth::new();
    let resource = examples::foobars__id_::Resource_id::from_static(provisioned_id);

    example::foobars__id_::get(&resource, &auth);
}

依赖

~10–19MB
~314K SLoC