#open-api #rest #rest-client #oas #api-bindings

tapioca

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

2个版本

使用旧的Rust 2015

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

#65#rest-client

自定义许可

14KB
347

Tapioca

Typed API s(Oliver Coshed into an Acronym)

Crate Build Status

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

它使用 OpenAPI Initiative的架构规范 来推断路径和查询参数、请求和响应正文等类型,然后使用 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 还旨在防止你因无效的 序列 请求而自伤,例如在特定资源上在 DELETE 后执行 GET:这是通过仅从响应和静态值中构建资源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 中,但tapioca使用的其他crate(不是用于宏的)不需要这种处理,也不会污染您的项目命名空间。

访问客户端

infer_api 构建的模块包含了API上每个路径的模块,每个模块都包含针对该资源的每个有效HTTP方法的函数。例如,对 GET /foobars 的函数标识符是

example::foobars::get

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

example::foobars::get::QueryParams

认证

在发送请求之前,我们需要介绍认证——目前,每个请求都必须指定认证,即使它是null。

在OAS模式中,认证需求在两个级别上进行指定:全局服务器级和操作级——GET /foobars 可以有不同的需求,而其他操作可能只是继承自服务器需求。因此,我们有两种可接受的认证方案的枚举类型

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
  • 布尔值
  • 字符串
  • 可选<_>
  • 向量<_>
  • 进一步的 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
~311K SLoC