2个不稳定版本
0.2.0 | 2023年12月30日 |
---|---|
0.1.0 | 2023年11月13日 |
#941 in HTTP服务器
205KB
3K SLoC
Rust Pokedex API 🦀
本项目实现了一个简单的Web应用程序,其中包含一个用于Pokedex(宝可梦数据库)的CRUD API。它用Rust编程语言编写,旨在作为在该语言中构建完整工作的Web应用程序的实验。
Rust是一种系统编程语言,具有低资源占用和优秀性能,与C等其他系统语言相比,它还包括内存安全特性,使其成为Java、Ruby或JavaScript等高级语言构建Web应用程序的吸引人替代品。
本项目包含现代Web应用程序中常见的几个组件,包括
- 用于处理传入请求的高性能HTTP服务器
- 为宝可梦实体提供CRUD端点的REST API
- 自动将宝可梦实体序列化和反序列化为JSON
- 自动生成OpenAPI文档,包括Swagger UI支持(及其他)
- 用于在Postgres数据库中持久化宝可梦的ORM-like界面
- 支持管理和应用数据库迁移
- 在端点级别验证传入数据
- 数据库连接池以提高性能
- 使用简单的日志外观进行可配置的日志记录
- 服务错误与其HTTP响应对应物的分离错误处理
- 支持开发和生产环境
构建和运行
支持的平台
理论上,该Web应用程序应在Rust支持的所有平台上运行。然而,Windows不支持原生运行Linux Docker容器。在Windows上运行应用程序的最简单方法是使用WSL。否则,基于Docker的命令将无法直接使用。这包括运行本地Postgres数据库,它需要手动安装。
先决条件
为了构建和运行Web应用程序和相关实用程序,您至少需要
- 安装 Docker Engine,包括 Docker Compose。如果您还没有安装,最简单的方法是安装 Docker Desktop。(如前所述,在 Windows 上,原生的 Docker Desktop 不会工作;但是您可以在 WSL 上使用 Docker。)
- just 命令行运行器。这个类似
make
的工具用于运行项目特定的命令。它可以以多种方式安装,详情见 这里。
使用 Docker
按照以下步骤使用 Docker 构建、设置和运行服务。在这种情况下,不需要在本地安装 Rust。
构建镜像
just docker-build
这将创建一个名为 clechasseur/pokerust
的本地 Docker 镜像,其中包含 Web 应用程序二进制文件和相关工具。它运行在 Debian Linux 上。第一次构建可能需要一段时间,因为您需要下载构建 Docker 镜像并编译所有代码。(如果您不习惯使用编译型语言,这可能会觉得时间很长,但请耐心等待 😉)
启动本地数据库
just db up
这将启动两个本地容器,运行 Postgres。一个将作为运行本地 Web 应用程序时的数据库服务器;另一个用于运行集成测试(如果您以后想这么做)。
当您完成本地数据库的使用后,您可以停止它
just db down
运行数据库迁移
just docker-migrate
这将执行一个名为 run_migrations
的小工具(在上面的 Docker 镜像中编译),在本地运行的 Postgres 数据库上运行数据库迁移。这将设置数据库,使其准备好供 Web 应用程序使用。
数据库初始化(可选)
just docker-seed
这将执行一个名为 seed_db
的小工具(在上面的 Docker 镜像中编译),它将读取包含 800 个宝可梦数据的 CSV 文件 并将其插入本地数据库。首先会清除数据库中现有的任何数据。此步骤是可选的,但可以用来展示 REST API 的可能性,而无需手动插入许多宝可梦。
启动宝可梦图鉴服务器
just docker-serve
这将启动 Web 应用程序服务器,本地监听端口 8080。您应该在控制台看到日志条目,包括一条表示服务器已成功启动的消息
[2023-10-29T03:38:50Z INFO pokedex_rs] Pokedex server started in Production! Listening on 0.0.0.0:8080.
之后,API 可以在 /api/v1/pokemons
访问。您还可以通过访问 应用程序的 Swagger UI 来查看支持哪些端点。
本地
为了本地构建和运行应用程序,您需要以下额外的组件
- 一个最新的稳定 Rust 工具链(至少需要 1.70.0)。如果您还没有安装 Rust,最简单的方法是通过 rustup 安装。
libpq
库(Postgres 的 C 接口)。如果您没有在本地安装,可以以多种方式安装,包括- Homebrew(macOS / Linux):
brew install libpq
- 基于 Debian 的 Linux:
sudo apt install libpq-dev
- Homebrew(macOS / Linux):
- 如果您想与数据库架构一起工作,您需要Diesel CLI。不过,运行数据库迁移不是必须的,因为本地构建的
run_migrations
工具也能完成这项工作。
默认情况下,Diesel CLI需要一些本地库来支持Postgres、MySQL和SQLite;但是,Pokédex只需要Postgres的支持。要仅安装带有Postgres支持的CLI,您可以运行:
cargo install diesel_cli --no-default-features --features "postgres"
- 如果您想运行
rustfmt
或构建文档,您需要一个Nightly Rust工具链。如果您没有,可以通过运行以下命令安装:
rustup toolchain install nightly
- 如果您想在本地运行测试并生成代码覆盖率,您需要安装
cargo-tarpaulin
。如果您还没有安装,可以通过以下多种方式之一进行安装: - 如果您想本地确定项目的最低支持Rust版本(MSRV),您需要安装
cargo-msrv
。如果您还没有安装,可以通过以下多种方式之一进行安装:
请注意,运行cargo-msrv
将在本地安装大量的Rust工具链。
构建二进制文件
just build
这将在target/
文件夹中构建Web应用程序和相关二进制文件。第一次可能需要一些时间。
启动和设置本地数据库
即使是在本地构建应用程序,您仍然需要一个Postgres数据库来存储Pokédex数据。最简单的方法是通过Docker:
just db up
在Windows(原生)上运行时,这需要手动完成。请参阅.env
文件和测试app.rs
文件以获取有关应用程序对本地数据库期望的详细信息。
然后,就像上面一样,您需要运行迁移(可选)并(可选)初始化数据库
just migrate seed
像之前一样,当您完成本地数据库操作后,您可以停止它
just db down
启动本地服务器
just serve
这将启动本地应用程序服务器。和之前一样,它可以通过/api/v1/pokemons
访问。
如果您检查控制台日志,您可能会注意到,在本地运行服务器时,它以Development
模式启动
[2023-10-29T04:16:21Z INFO pokedex_rs] Pokedex server started in Development! Listening on 127.0.0.1:8080.
这会影响API返回的错误信息的内容(见下文)。
运行测试
just test
这将运行项目中包含的所有测试:单元测试、集成测试(需要本地测试数据库运行)和文档测试(Rust的一个酷特性,允许您将测试嵌入到代码的文档中)。
要运行带有代码覆盖率的测试,使用以下命令:
just tarpaulin
这将运行所有测试,并在项目根目录下生成名为tarpaulin-report.html
的HTML报告。请注意,这需要更长时间,因为代码覆盖率需要一个特殊的构建(带有仪器)并且因为一个明显的错误,每次运行都需要重新构建测试 😔。
生成文档
just doc
这将通过rustdoc
生成代码中使用的类型和函数的文档。生成的HTML将在您的本地网络浏览器中打开。
rustdoc
生成相当全面和优秀的文档。例如,您可以查看 在 docs.rs 上的 actix-web
文档。
代码检查和格式化
Rust 提供了两个工具来帮助您检查代码:
clippy
:Rust 代码检查器。检查代码中的常见错误,虽然这些错误并非真正的 bug,但可以进行改进。rustfmt
:Rust 代码格式化工具。根据预定义的规则自动格式化代码,这些规则可以被 配置。
您可以在代码库上运行这两个工具。
just tidy
功能
本节探讨了项目中代码中的一些有趣功能。
OpenAPI 支持
应用程序包括对通过 utoipa
crate 生成 API 的 OpenAPI 3.0 文档的支持(注意:这不是一个拼写错误)。当应用程序运行时,可以在 /api-docs/openapi.json
访问文档。文档还可以通过内置的前端查看。
- Swagger UI(通过
/swagger-ui/
) - Redocly(通过
/redoc
) - RapiDoc(通过
/rapidoc
)
开发模式下的内部错误
应用程序可以以两种模式运行:Development
(开发模式)或 Production
(生产模式)。默认情况下,它以生产模式运行,但可以通过 POKEDEX_ENV
环境变量设置模式。在本地存储库中,它通过 .env
文件 设置为 Development
。
当应用程序以 Development
模式运行时,API 返回的错误将包含一个 internal_error
字段,其中包含导致错误返回的递归错误消息。
例如,如果在数据库关闭时运行 API 查询
% just db down
docker compose down
[+] Running 3/3
✔ Container pokerust-pokedex-db-test-1 Removed
✔ Container pokerust-pokedex-db-1 Removed
✔ Network pokerust_pokedex-net Removed
% curl https://127.0.0.1:8080/api/v1/pokemons | jq
{
"status_code": 500,
"error": "Internal Server Error",
"internal_error": "database connection error\ncaused by: Error occurred while creating a new object: error connecting to server: Connection refused (os error 61)\ncaused by: error connecting to server: Connection refused (os error 61)"
}
出于安全原因,当以 Production
模式运行时,不会返回 internal_error
字段,因为这可能会泄露安全细节。但是,一些错误(如验证错误)仍然包含一个包含更多信息的 details
字段。
% curl https://127.0.0.1:8080/api/v1/pokemons/-1 | jq
{
"status_code": 400,
"error": "Bad Request",
"details": "Validation error: id: Validation error: range [{\"value\": Number(-1), \"min\": Number(0.0)}]"
}
回溯支持
Rust 支持在发生错误时生成“回溯”(例如调用栈)。然而,尽管 Backtrace
结构体 已在稳定 Rust 中提供,但在 Nightly Rust 中才支持在错误发生时存储回溯。
应用程序支持在 Development
模式下将回溯包含在错误中。这需要两个条件
- 使用 Nightly 工具链构建应用程序
- 设置
RUST_BACKTRACE
环境变量(设置为1
)以启用回溯捕获(否则,回溯将是空的)
在本地测试回溯支持
RUST_BACKTRACE=1 just toolchain=nightly serve
这将使用Nightly Rust工具链构建和运行应用程序,并启用回溯生成。可以在服务器日志中验证回溯支持。
[2023-10-29T04:56:08Z INFO pokedex_rs] Rust version used: 1.75.0-nightly
[2023-10-29T04:56:08Z INFO pokedex_rs] Backtrace support: supported
通过向API发送无效查询,可以测试包含错误信息的回溯。
curl https://127.0.0.1:8080/api/v1/pokemons/-1
通过Docker测试回溯支持
通过Docker进行测试稍微复杂一些,因为它需要使用Nightly Rust工具链为应用程序构建另一个Docker镜像。
just toolchain=nightly docker-build
这将构建应用程序的另一个Docker镜像版本(clechasseur/pokerust:nightly
);同样,这第一次可能需要一些时间。要使用该镜像运行应用程序并启用回溯支持,请使用
just toolchain=nightly docker-serve --env POKEDEX_ENV=development --env RUST_BACKTRACE=1
然后,就像之前一样,您可以测试回溯支持是否正常工作
curl https://127.0.0.1:8080/api/v1/pokemons/-1
您可能会注意到返回的回溯与本地生成的回溯不同。这是因为回溯生成高度依赖于平台(甚至在某些平台上完全不支持)。
日志级别
Pokédex应用程序包括各种操作的记录。与其他流行的框架一样,日志条目有不同的级别:error
、warning
、info
、debug
或trace
(见log::Level
)。
默认情况下,应用程序仅显示级别为info
或以上的日志条目。但是,可以通过RUST_LOG
环境变量进行配置。例如,在本地运行时启用trace
日志
RUST_LOG=trace just serve
或通过Docker
just docker-serve --env RUST_LOG=trace
存在许多其他选项来控制日志输出,包括过滤某些条目以及仅针对特定模块启用日志。有关更多信息,请参阅env_logger
crate文档。
分页支持
GET /api/v1/pokemons
端点支持按页列出Pokédex中的Pokémon。默认情况下,端点一次返回最多10个Pokémon。分页通过page
和page_size
查询参数控制。例如
curl "https://127.0.0.1:8080/api/v1/pokemons?page=2&page_size=5"
返回的JSON将包括Pokémon以及关于指定page_size
可用的总页数信息
{
"pokemons": [
...
],
"page": 2,
"page_size": 5,
"total_pages": 160
}
出于性能考虑,page_size
受到限制(目前限制为100)。这目前是在服务代码中硬编码的(请参阅service/pokemon.rs
中的MAX_PAGE_SIZE)。
文档
虽然Pokédex应用程序是一个bin crate,但主程序的代码仅包括实际启动HTTP服务器并监听连接所必需的内容。代码的主体在项目的lib crate(请参阅lib.rs
)中,并且全部都有文档。如前所述,文档可以通过以下方式生成和查看
just doc
请注意,库中所有类型和函数目前都是公开的。这通常不是这种情况;这样做是为了更容易通过文档探索应用程序的代码。
集成测试
该项目包含集成测试,使用 actix-web
的测试助手启动测试服务,将其连接到独立 Postgres 服务器上的测试数据库。集成测试在真实的 API 端点上执行请求,并解析数据以验证结果。
为了使测试能够对实体数量等进行验证,每个测试都会创建一个新的测试服务,并在测试结束时清除测试数据库的内容。这效果不错,但有一个缺点:因为测试实际上会将更改持久化到数据库中,所以它们会相互干扰,因此必须 序列化(例如,它们不能并行运行)。
在这个示例项目中,测试数量很少,因此可以管理。然而,在一个大型项目中,这会成为一个大问题。许多框架通过在将连接传递给测试代码之前创建数据库连接并开始一个 事务 来解决这个问题,然后在测试完成后回滚事务。由于没有实际数据被提交到数据库中,测试可以很容易地并行运行。
尽管如此,我还没有找到一种简单的方法来实现这一点。API 端点使用的数据库连接来自连接池。此外,一些测试执行多个请求,因此它们必须在整个测试过程中复用相同的连接,以便它们处于同一个事务中。我不太确定如何将 API 代码钩入以实现这一点。我有种感觉,使用 mockall_double
可以有所帮助,但我还没有花足够的时间去考虑它。
有趣的库
在 Rust 中,外部库存储在称为 crates 的单元中。许多开源库可在 crates.io 注册表 中查看和下载。不过,无需手动下载;Rust 的包管理器 cargo
在构建时会自动查找依赖项,并在 Cargo.toml
中查找。
与许多生态系统不同,Rust 生态系统不包括用于开发 Web 应用的万能框架(如 Ruby 的 Rails
框架或 Elixir 的 Phoenix
)。相反,Rust 库往往被拆分成小而可重用的组件,提供一到多个相关功能。因此,构建 Web 应用需要使用多个库(构建这个实验性项目的大部分时间都花在寻找和测试应用不同部分的库上)。
以下列表包含了一些在应用项目中使用的有趣库。它们对于构建类似的 Rust 项目(甚至一些与 Rust 生态系统无关的 Rust 项目)非常有用。
Web 框架
actix-web
可能是 Rust 最受欢迎的 Web 框架。它提供卓越的性能,同时仍然相对容易设置和使用。有关 Web 开发的其他选项,请查看 Are we web yet? 网站。
actix-web
使用 Rust 的 异步编程 支持以高效的方式处理请求。这需要一个异步运行时。进入 tokio
,这是一个专为构建网络应用程序设计的异步运行时。尽管 Rust 生态系统中有其他异步运行时实现,但 tokio
目前是最广泛使用的。
数据库和持久化
diesel
:一个灵活的 ORM 框架deadpool
:一个用于异步池化数据库连接(或任何类型对象)的库diesel-async
:围绕diesel
的异步包装器,包括连接池支持
diesel
可能是 Rust 生态系统中最受欢迎的 ORM。然而,diesel
的问题之一是它不提供异步接口;这意味着当你在异步函数中执行数据库操作时,运行时线程池中的线程将一直挂起,直到 DB 调用返回。然而,这究竟是不是一个真正的“问题”是有争议的;异步代码本身并不一定更快。
此项目使用 diesel-async
crate 来包装使用 diesel
执行的数据库调用,以便它们看起来是异步的。然而,这主要是“为了显示”,因为 diesel
仍然是同步的。相反,DB 调用被卸载到不属于运行时线程池的其他线程。这种改进是否显著需要通过基准测试来验证。
diesel
的替代方案包括
sqlx
:一个使用异步接口执行 SQL 查询的库,尽管没有 DSLormx
:一个小型库,向sqlx
添加类似 ORM 的功能,尽管方式有限sea-orm
:一个真正的异步 ORM,底层使用sqlx
sea-orm
看起来很有前景,并且似乎比 diesel
需要更少的样板代码。它还支持将数据库迁移编写为 Rust 代码而不是纯 SQL(是否更好是一个有争议的问题)。
验证器
validator
:一个简单的库,用于为 Rust 结构体添加验证支持actix-web-validator
:为actix-web
项目添加validator
支持,允许自动验证 API 输入
这两个 crate 的组合允许代码在结构体级别添加验证,然后在 API 级别强制执行。然后可以通过 Actix 的内置错误处理功能将验证错误转换为适当的 HTTP 响应(例如 400 Bad Request
)。
OpenAPI 支持
utoipa
:你的 API 的 OpenAPI 3.0 文档生成器(附带一个古怪的名字)utoipa-swagger-ui
:通过utoipa
自动支持 Swagger UIutoipa-redoc
:通过utoipa
自动支持 Redoclyutoipa-rapidoc
:通过utoipa
自动支持 RapiDoc
使用 utoipa
为您的API端点生成OpenAPI文档非常简单。它允许您在端点以及模式和方法响应结构上使用派生宏进行文档化,然后将所有内容绑定在一起以生成一个OpenAPI JSON文档。然后,可以使用其他crate中的Swagger UI等查看器来使用此文档。
我在使用 utoipa
时发现了一些问题(例如,许多派生宏使用 rustdoc
文档来生成OpenAPI文档,但有时您希望它不同);然而,它确实是一个非常节省时间的方法。
序列化
serde
:Rust生态系统的默认序列化/反序列化库serde_json
:使用serde
允许(反)序列化Rust类型的JSON解析器和验证器csv
:支持通过serde
进行(反)序列化的CSV解析器serde_with
/serde-this-or-that
:实现serde
支持的帮助工具
在支持某种反射API的语言中,以JSON等格式序列化数据结构更为容易。编译型语言可以具有这种支持(例如:Java),但不幸的是,Rust并不是这样。然而,这通常可以用泛型特性和聪明的proc宏来代替。
让我们看看 serde
,这是一个编译时序列化库,通过实现一个无格式序列化框架展示了Rust的 特质系统 的强大功能。
基本上,serde
将类型的 序列化 分离为基本指令和将数据持久化在特定格式中的序列化器实现。为此,serde
提供了通用的 Serialize
特质,该特质可以被实现。这个特质的唯一方法,serialize
,接收一个 Serializer
(另一个通用特质)并将其用于保存类型的数据。例如,一个结构体会通过序列化结构体,然后是每个命名的(或未命名的)字段来持久化自己。
然后,其他crate如 serde_json
为其特定数据格式提供 Serializer
特质的实际实现。这些序列化器将提供的序列化指令转换为适当的输出。魔法!
但乐趣并未结束。因为 serde
默认支持序列化大多数基本Rust类型,所以 Serialize
特质可以自动为几乎所有类型 派生。例如,只要结构体和枚举包含可以自身序列化的字段(例如,它们的类型已经实现了 Serialize
),就可以为它们派生 Serialize
。在实践中,这意味着可以通过简单地标记一个结构体,例如使用 #[derive(Serialize)]
,就可以自动在所有由基于 serde
的库支持的数据格式(很多)中进行序列化。
(反序列化通过 Deserialize
和 Deserializer
特质进行类似的支持。)
日志记录
log
:Rust的简单日志外观env_logger
:可以通过环境变量配置的控制台日志记录器simple_logger
:用于简单情况的极简控制台日志记录器
log
是在 Rust 生态系统中广泛使用的日志封装器。它包含易于使用的宏来记录数据,例如 info!
、error!
等。然后,为了执行实际的日志记录,您可以在程序开始时初始化一个日志实现(例如 env_logger
)。
存在多个日志实现;特别是,一些可以用于将日志记录到文件中。它们在本项目中没有探讨,但在 log
crate 文档 中可以找到一些。
错误处理
对于那些习惯于通过异常处理错误的人来说,Rust 的错误处理能力可能一开始会感觉有些奇怪(它们与 Go 等语言更相似)。
在 Rust 中,处理错误的基本方式是使用一个名为 Result
的类型。 Result
是一个 enum - Rust 中的一个概念,类似于 Java 或 C++ 等其他语言中的枚举,但实际上更强大:在 Rust 中,每个枚举变体都可以选择性地包含额外的数据,并且枚举“知道”它在任何时刻存储的是哪个变体。枚举变体中的数据不是共享的,因此一次只能有一个变体的数据成员(有点像 C 中的联合)。
Result
枚举只有两个变体
Ok(T)
:表示成功,包含类型为T
的结果数据Err(E)
:表示错误,包含类型为E
的错误信息
当一个函数是可错误的,它通常返回一个 Result
,可以用来确定调用是否成功。如果函数返回 Err
,则表示发生了错误,必须对其进行处理。因为 Result
是泛型但强类型,错误可以向上传递,但它们的类型将被清楚地识别。
Rust 还包括一个称为 Error
的特质,通常用于错误类型(尽管不是必需的)。这个特质的目标是能够获取错误的“来源”——根本原因的错误。在许多语言中,这实际上被称为 cause
。
对于外部库,通常需要定义一个自定义错误类型,该类型实现Error
,并且可以用来表示库返回的不同类型的错误(通常通过枚举)。这就是thiserror
库发挥作用的地方:它提供了一个 derive 宏,可以自动为您的错误类型实现Error
trait(以及一些相关的 trait,如Display
)。
对于应用程序,有时我们可能希望能够处理任何类型的错误,因为我们可能需要调用许多不同的库,因此创建一个自定义类型可能会变得难以控制。《anyhow》库可以用于此目的:它的anyhow::Error
类型可以用来存储任何源错误,只要它实现了标准库的Error
trait。这使得在应用程序级别添加适当的错误处理变得更加容易。
Rust的错误处理设计意味着您需要仔细考虑如何在代码中处理错误:当深层层发生错误时,您是否应该原样向上传递?您是否应该将其包装在一个更友好的错误类型中以便添加上下文?也许您可以通过其他方式简单地进行补偿?尽管这在所有应用程序中都应该是必须存在的,但 Rust 对实际返回类型为Result
(而不是通过可以轻易在层之间传播的异常)的依赖性意味着您必须仔细考虑它。这可能会在开始时感到有些令人畏惧,但可以争辩说,您库的最终 API 将更加稳固。
贡献
虽然这个项目仅作为一个例子,但如果您想讨论它,请随时打开一个讨论(如果您在某个地方发现了一个错误,请打开一个问题)。另请参阅CODE_OF_CONDUCT.md
。
依赖关系
~26–42MB
~690K SLoC