控制器

Loco 是一个框架,它封装了 axum,提供了一种直接的方式来管理路由、中间件、身份验证以及更多开箱即用的功能。在任何时候,你都可以利用强大的 axum Router,并使用你自定义的中间件和路由对其进行扩展。

Controllers 和路由

添加一个 Controller

提供了一个方便的代码生成器,以简化创建连接到你的项目的 starter controller。此外,还会生成一个测试文件,以便轻松测试你的 controller。

生成一个 controller:

$ cargo loco generate controller [OPTIONS] <CONTROLLER_NAME>

生成 controller 后,导航到 src/controllers 中创建的文件,以查看 controller 端点。你还可以查看测试(在 tests/requests 文件夹中)文档,以了解如何测试此 controller。

显示激活的路由

要查看所有已注册 controller 的列表,请执行以下命令:

$ cargo loco routes

[GET] /_health
[GET] /_ping
[POST] /auth/forgot
[POST] /auth/login
[POST] /auth/register
[POST] /auth/reset
[POST] /auth/verify
[GET] /auth/current

此命令将为你提供系统中当前注册的 controller 的全面概述。

添加状态

你的应用程序上下文和状态保存在 AppContext 中,这是 Loco 为你提供和设置的。在某些情况下,你可能希望在应用程序启动时加载自定义数据、逻辑或实体,并在所有 controller 中可用。

你可以通过使用 Axum 的 Extension 来做到这一点。这是一个加载 LLM 模型的示例,这是一个耗时的任务,然后将其提供给 controller 端点,在那里它已经被加载,并且可以立即使用。

首先,在 src/app.rs 中添加一个生命周期钩子:

    // 在 src/app.rs 中,在你的 Hooks trait 实现中重写 `after_routes` 钩子:

    async fn after_routes(router: axum::Router, _ctx: &AppContext) -> Result<axum::Router> {
        // 缓存应该位于:~/.cache/huggingface/hub
        println!("loading model");
        let model = Llama::builder()
            .with_source(LlamaSource::llama_7b_code())
            .build()
            .unwrap();
        println!("model ready");
        let st = Arc::new(RwLock::new(model));

        Ok(router.layer(Extension(st)))
    }

接下来,在你喜欢的任何地方使用这个状态扩展。这是一个 controller 端点的示例:

async fn candle_llm(Extension(m): Extension<Arc<RwLock<Llama>>>) -> impl IntoResponse {
    // 从你的状态扩展中使用 `m`
    let prompt = "write binary search";
    ...
}

全局应用范围的状态

有时你可能需要可以在 controller、worker 和应用程序的其他区域之间共享的状态。

你可以查看示例 shared-global-state 应用程序,以了解如何集成 libvips,这是一个基于 C 的图像处理库。libvips 对开发者提出了一个奇怪的要求:在每个应用程序进程中保持加载它的单个实例。我们通过保留一个 single lazy_static field 来实现这一点,并在应用程序的不同位置引用它。

阅读以下内容,了解如何在应用程序的每个部分中完成此操作。

Controller 中的共享状态

你可以使用本文档中提供的解决方案。一个实时的例子在这里

Worker 中的共享状态

Worker 在 app hooks 中被有意地逐字初始化。

这意味着你可以将它们塑造为“常规”的 Rust 结构体,该结构体将状态作为字段。然后在 perform 中引用该字段。

这是 worker 如何在 shared-global-state 示例中使用全局 vips 实例初始化的

请注意,根据设计,在 controller 和 worker 之间共享状态没有意义,因为即使你最初可以选择在与 controller 相同的进程中运行 worker(并共享状态)——你也会希望在水平扩展时快速切换到由队列支持并在独立的 worker 进程中运行的适当的 worker,因此 worker 根据设计应该与 controller 没有共享状态,这对你有利。

Task 中的共享状态

Task 实际上没有共享状态的价值,因为它们的生命周期与任何 exec 的二进制文件类似。进程启动、引导、创建所有需要的资源(连接到数据库等)、执行 task 逻辑,然后

Controller 中的路由

Controller 定义了 Loco 路由功能。在下面的示例中,一个 controller 创建了一个 GET 端点和一个 POST 端点:

use axum::routing::{get, post};
Routes::new()
    .add("/", get(hello))
    .add("/echo", post(echo))

你还可以使用 prefix 函数为 controller 中的所有路由定义一个 prefix

发送响应

响应发送器位于 format 模块中。以下是从你的路由发送响应的几种方法:


// 保持返回 `Result<impl IntoResponse>` 的最佳实践,以便能够透明地交换返回类型
pub async fn list(...) -> Result<impl IntoResponse> // ..

// 使用 `json`、`html` 或 `text` 进行简单响应
format::json(item)


// 使用 `render` 为更复杂的响应提供构建器接口。你仍然可以使用
// `json`、`html` 或 `text` 终止
format::render()
    .etag("foobar")?
    .json(Entity::find().all(&ctx.db).await?)

内容类型感知的响应

你可以选择加入响应器机制,其中会检测格式类型并将其传递给你。

使用 Format 提取器来实现此目的:

pub async fn get_one(
    respond_to: RespondTo,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    let res = load_item(&ctx, id).await?;
    match respond_to {
        RespondTo::Html => format::html(&format!("<html><body>{:?}</body></html>", item.title)),
        _ => format::json(item),
    }
}

自定义错误

这里有一个案例,你可能希望根据不同的格式进行不同的渲染,并且还希望根据你收到的错误类型进行不同的渲染。

pub async fn get_one(
    respond_to: RespondTo,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    // 拥有 `load_item` 非常有用,因为在函数内部你可以调用和使用
    // '?' 来冒泡错误,然后,在这里,我们集中处理错误。
    // 如果你想自由地使用没有包装函数的代码语句,你可以
    // 使用 Rust 中的实验性 `try` 功能,你可以这样做:
    // ```
    // let res = try {
    //     ...
    //     ...
    // }
    //
    // match res { ..}
    // ```
    let res = load_item(&ctx, id).await;

    match res {
        // 我们很好,让我们根据内容类型渲染 item
        Ok(item) => match respond_to {
            RespondTo::Html => format::html(&format!("<html><body>{:?}</body></html>", item.title)),
            _ => format::json(item),
        },
        // 我们对如何渲染验证错误有自己的看法,仅在 HTML 内容中
        Err(Error::Model(ModelError::ModelValidation { errors })) => match respond_to {
            RespondTo::Html => {
                format::html(&format!("<html><body>errors: {errors:?}</body></html>"))
            }
            _ => bad_request("opaque message: cannot respond!"),
        },
        // 我们不知道这是什么,让框架渲染默认错误
        Err(err) => Err(err),
    }
}

在这里,我们还通过首先将工作流程包装在一个函数中并获取结果类型来“集中”我们的错误处理。

接下来,我们创建一个 2 级匹配来:

  1. 匹配结果类型
  2. 匹配格式类型

在我们缺乏处理知识的地方,我们只是按原样返回错误,并让框架渲染默认错误。

手动创建 Controller

1. 创建 Controller 文件

首先在路径 src/controllers 下创建一个新文件。例如,让我们创建一个名为 example.rs 的文件。

2. 在 mod.rs 中加载文件

确保你在 src/controllers 文件夹中的 mod.rs 文件中加载新创建的 controller 文件。

3. 在 App Hooks 中注册 Controller

在你的 App hook 实现(例如,App 结构体)中,将你的 controller 的 Routes 添加到 AppRoutes

// src/app.rs

pub struct App;
#[async_trait]
impl Hooks for App {
    fn routes() -> AppRoutes {
        AppRoutes::with_default_routes().prefix("prefix")
            .add_route(controllers::example::routes())
    }
    ...
}

中间件

Loco 开箱即用地提供了一组内置中间件。有些默认启用,而另一些则需要配置。中间件注册非常灵活,可以通过 *.yaml 环境配置或直接在代码中进行管理。

默认堆栈

你将获得所有已启用的中间件,运行以下命令

cargo loco middleware --config

这是 development 模式下的堆栈:

$ cargo loco middleware --config

limit_payload          {"body_limit":{"Limit":1000000}}
cors                   {"enable":true,"allow_origins":["any"],"allow_headers":["*"],"allow_methods":["*"],"max_age":null,"vary":["origin","access-control-request-method","access-control-request-headers"]}
catch_panic            {"enable":true}
etag                   {"enable":true}
logger                 {"config":{"enable":true},"environment":"development"}
request_id             {"enable":true}
fallback               {"enable":true,"code":200,"file":null,"not_found":null}
powered_by             {"ident":"loco.rs"}


remote_ip              (disabled)
compression            (disabled)
timeout                (disabled)
static_assets          (disabled)
secure_headers         (disabled)

示例:禁用所有中间件

采用任何已启用的中间件,并在相关字段中使用 enable: false。如果 server 中的 middlewares: 部分缺失,请添加它。

server:
  middlewares:
    cors:
      enable: false
    catch_panic:
      enable: false
    etag:
      enable: false
    logger:
      enable: false
    request_id:
      enable: false
    fallback:
      enable: false

结果:

$ cargo loco middleware --config
powered_by             {"ident":"loco.rs"}


cors                   (disabled)
catch_panic            (disabled)
etag                   (disabled)
remote_ip              (disabled)
compression            (disabled)
timeout_request        (disabled)
static                 (disabled)
secure_headers         (disabled)
logger                 (disabled)
request_id             (disabled)
fallback               (disabled)

你可以通过更改 server.ident 的值来控制 powered_by 中间件:

server:
    ident: my-server #(或空字符串禁用)

示例:添加一个非默认中间件

让我们将 Remote IP 中间件添加到堆栈中。这只需通过配置即可完成:

server:
  middlewares:
    remote_ip:
      enable: true

结果:

$ cargo loco middleware --config

limit_payload          {"body_limit":{"Limit":1000000}}
cors                   {"enable":true,"allow_origins":["any"],"allow_headers":["*"],"allow_methods":["*"],"max_age":null,"vary":["origin","access-control-request-method","access-control-request-headers"]}
catch_panic            {"enable":true}
etag                   {"enable":true}
remote_ip              {"enable":true,"trusted_proxies":null}
logger                 {"config":{"enable":true},"environment":"development"}
request_id             {"enable":true}
fallback               {"enable":true,"code":200,"file":null,"not_found":null}
powered_by             {"ident":"loco.rs"}

示例:更改已启用中间件的配置

让我们将请求体限制更改为 5mb。当覆盖中间件配置时,请记住保留 enable: true

  middlewares:
    limit_payload:
      body_limit: 5mb

结果:

$ cargo loco middleware --config

limit_payload          {"body_limit":{"Limit":5000000}}
cors                   {"enable":true,"allow_origins":["any"],"allow_headers":["*"],"allow_methods":["*"],"max_age":null,"vary":["origin","access-control-request-method","access-control-request-headers"]}
catch_panic            {"enable":true}
etag                   {"enable":true}
logger                 {"config":{"enable":true},"environment":"development"}
request_id             {"enable":true}
fallback               {"enable":true,"code":200,"file":null,"not_found":null}
powered_by             {"ident":"loco.rs"}


remote_ip              (disabled)
compression            (disabled)
timeout_request        (disabled)
static                 (disabled)
secure_headers         (disabled)

身份验证

Loco 框架中,中间件在身份验证中起着至关重要的作用。Loco 支持各种身份验证方法,包括 JSON Web Token (JWT) 和 API Key 身份验证。本节概述如何在你的应用程序中配置和使用身份验证中间件。

JSON Web Token (JWT)

配置

默认情况下,Loco 对 JWT 使用 Bearer 身份验证。但是,你可以在配置文件中的 auth.jwt 部分自定义此行为。

  • Bearer 身份验证: 将配置留空或显式设置为如下:
    # 身份验证配置
    auth:
      # JWT 身份验证
      jwt:
        location: Bearer
    ...
    
  • Cookie 身份验证: 配置从中提取 token 的位置并指定 cookie 名称:
    # 身份验证配置
    auth:
      # JWT 身份验证
      jwt:
        location:
          from: Cookie
          name: token
    ...
    
  • 查询参数身份验证: 指定查询参数的位置和名称:
    # 身份验证配置
    auth:
      # JWT 身份验证
      jwt:
        location:
          from: Query
          name: token
    ...
    
用法

在你的 controller 参数中,使用 auth::JWT 进行身份验证。这将根据配置的设置触发身份验证验证。

use loco_rs::prelude::*;

async fn current(
    auth: auth::JWT,
    State(_ctx): State<AppContext>,
) -> Result<Response> {
    // 你的实现代码
}

此外,你可以通过将 auth::JWT 替换为 auth::ApiToken<users::Model> 来获取当前用户。

API Key

对于 API Key 身份验证,请使用 auth::ApiToken。此中间件根据用户数据库记录验证 API Key,并将相应的用户加载到身份验证参数中。

use loco_rs::prelude::*;

async fn current(
    auth: auth::ApiToken<users::Model>,
    State(_ctx): State<AppContext>,
) -> Result<Response> {
    // 你的实现代码
}

Catch Panic

此中间件捕获应用程序请求处理期间发生的 panic。当发生 panic 时,它会记录错误并返回内部服务器错误响应。此中间件有助于确保应用程序可以优雅地处理意外错误,而不会导致服务器崩溃。

要禁用中间件,请按如下方式编辑配置:

#...
  middlewares:
    catch_panic:
      enable: false

Limit Payload

Limit Payload 中间件限制 HTTP 请求 payload 允许的最大大小。默认情况下,它已启用并配置为 2MB 限制。

你可以通过配置文件自定义或禁用此行为。

设置自定义限制

#...
  middlewares:
    limit_payload:
      body_limit: 5mb

禁用 payload 大小限制

要完全删除限制,请将 body_limit 设置为 disable

#...
  middlewares:
    limit_payload:
      body_limit: disable
用法

在你的 controller 参数中,使用 axum::body::Bytes

use loco_rs::prelude::*;

async fn current(_body: axum::body::Bytes,) -> Result<Response> {
    // 你的实现代码
}

Timeout

对应用程序处理的请求应用超时。该中间件确保请求不会超出指定的超时时间运行,从而提高应用程序的整体性能和响应能力。

如果请求超过指定的超时时长,中间件将向客户端返回 408 Request Timeout 状态代码,表明请求处理时间过长。

要启用中间件,请按如下方式编辑配置:

#...
  middlewares:
    timeout_request:
      enable: false
      timeout: 5000

Logger

为 HTTP 请求提供日志记录功能。有关每个请求的详细信息,例如 HTTP 方法、URI、版本、用户代理和关联的请求 ID。此外,它还将应用程序的运行时环境集成到日志上下文中,从而允许特定于环境的日志记录(例如,“development”、“production”)。

要禁用中间件,请按如下方式编辑配置:

#...
  middlewares:
    logger:
      enable: false

Fallback

当选择 SaaS starter(或任何非 API-first 的 starter)时,你将获得具有 Loco 欢迎屏幕 的默认回退行为。这是一种仅限开发模式,其中 404 请求会向你显示一个友好且友好的页面,告诉你发生了什么以及下一步该怎么做。

你可以在 development.yaml 文件中禁用或自定义此行为。你可以设置几个选项:

# 默认的预制欢迎屏幕
fallback:
    enable: true
# 不同的预定义 404 页面
fallback:
    enable: true
    file: assets/404.html
# 一条消息,并将状态代码自定义为返回 200 而不是 404
fallback:
    enable: true
    code: 200
    not_found: cannot find this resource

对于生产环境,建议禁用此功能。

# 禁用。你也可以完全删除 `fallback` 部分以禁用
fallback:
    enable: false

Remote IP

当你的应用程序位于代理或负载均衡器(例如 Nginx、ELB 等)下时,它不会直接面向互联网,这就是为什么如果你想找出连接客户端 IP,你将获得一个套接字,该套接字指示的 IP 实际上是你的负载均衡器。

负载均衡器或代理负责处理针对真实客户端 IP 的套接字工作,然后通过代理回连将负载提供给你的应用程序。

这就是为什么当你的应用程序对获取真实客户端 IP 有具体的业务需求时,你需要使用事实上的标准代理和负载均衡器用于向你传递此信息的标头:X-Forwarded-For 标头。

Loco 提供了 remote_ip 部分来配置 RemoteIP 中间件:

server:
  middleware:
    # 当位于代理或负载均衡器后时,根据 `X-Forwarded-For` 计算远程 IP
    # 使用 RemoteIP(..) 提取器获取远程 IP。
    # 如果没有此中间件,你将获得代理 IP。
    # 更多信息:https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/middleware/remote_ip.rb
    #
    # 注意!仅在位于代理下时启用,否则可能会导致 IP 欺骗漏洞
    # 相信我,如果你需要此中间件,你会知道的。
    remote_ip:
      enable: true
      # # 替换默认的可信代理:
      # trusted_proxies:
      # - ip range 1
      # - ip range 2 ..
    # 生成唯一的请求 ID 并使用其他信息(例如请求处理的开始和完成、延迟、状态代码和其他请求详细信息)增强日志记录。

然后,使用 RemoteIP 提取器获取 IP:

#[debug_handler]
pub async fn list(ip: RemoteIP, State(ctx): State<AppContext>) -> Result<Response> {
    println!("remote ip {ip}");
    format::json(Entity::find().all(&ctx.db).await?)
}

当使用 RemoteIP 中间件时,请注意安全隐患与你当前的架构的对比(如文档和配置部分所述):如果你的应用程序不在代理下,你可能会容易受到 IP 欺骗漏洞的攻击,因为任何人都可以将标头设置为任意值,特别是,任何人都可以设置 X-Forwarded-For 标头。

默认情况下,此中间件未启用。通常,你 会知道 你是否需要此中间件,并且你将意识到在正确的架构中使用它的安全方面。如果你不确定——请不要使用它(保持 enablefalse)。

Secure Headers

Loco 带有默认的安全标头,由 secure_headers 中间件应用。这类似于 Rails 生态系统中使用 secure_headers 完成的操作。

在你的 server.middleware YAML 部分中,你将默认找到 github 预设(这是 Github 和 Twitter 推荐的安全标头)。

server:
  middleware:
    # 设置安全标头
    secure_headers:
      preset: github

你还可以覆盖选定的标头:

server:
  middleware:
    # 设置安全标头
    secure_headers:
      preset: github
      overrides:
        foo: bar

或者从头开始:

server:
  middleware:
    # 设置安全标头
    secure_headers:
      preset: empty
      overrides:
        foo: bar

为了支持 htmx,你可以添加以下覆盖,以允许一些内联运行脚本:

secure_headers:
    preset: github
    overrides:
        # 这允许你使用 HTMX,并具有 unsafe-inline。在生产环境中移除或考虑
        "Content-Security-Policy": "default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src 'unsafe-inline' 'self' https:; style-src 'self' https: 'unsafe-inline'"

Compression

Loco 利用 CompressionLayer 来实现 一键式 解决方案。

要启用基于 accept-encoding 请求标头的响应压缩,只需按如下方式编辑配置:

#...
  middlewares:
    compression:
      enable: true

这样做将压缩每个响应并相应地设置 content-encoding 响应标头。

预压缩资源

Loco 利用 ServeDir::precompressed_gzip 来实现 一键式 解决方案,以提供预压缩的资源。

如果静态资源以 .gz 文件的形式存在于磁盘上,则 Loco 将提供它,而不是动态压缩它。

#...
middlewares:
  ...
  static_assets:
    ...
    precompressed: true

CORS

此中间件通过允许 HTTP 请求中可配置的来源、方法和标头来启用跨域资源共享 (CORS)。 它可以根据各种应用程序需求进行定制,支持宽松的 CORS 或中间件配置中定义的特定规则。

#...
middlewares:
  ...
  cors:
    enable: true
    # 设置 [`Access-Control-Allow-Origin`][mdn] 标头的值
    # allow_origins:
    #   - https://loco.rs
    # 设置 [`Access-Control-Allow-Headers`][mdn] 标头的值
    # allow_headers:
    # - Content-Type
    # 设置 [`Access-Control-Allow-Methods`][mdn] 标头的值
    # allow_methods:
    #   - POST
    # 以秒为单位设置 [`Access-Control-Max-Age`][mdn] 标头的值
    # max_age: 3600

基于 Handler 和路由的中间件

Loco 还允许我们将 layers 应用于特定的 handler 或 路由。 有关基于 handler 和路由的中间件的更多信息,请参阅 middleware 文档。

基于 Handler 的中间件:

使用 layer 方法将 layer 应用于特定的 handler。

// src/controllers/auth.rs
pub fn routes() -> Routes {
    Routes::new()
        .prefix("auth")
        .add("/register", post(register).layer(middlewares::log::LogLayer::new()))
}

基于路由的中间件:

使用 layer 方法将 layer 应用于特定的路由。

// src/main.rs
pub struct App;

#[async_trait]
impl Hooks for App {
    fn routes(_ctx: &AppContext) -> AppRoutes {
        AppRoutes::with_default_routes()
            .add_route(
                controllers::auth::routes()
                    .layer(middlewares::log::LogLayer::new()),
            )
    }
}

请求验证

JsonValidate 提取器通过与 validator crate 集成,简化了输入验证。以下是如何验证传入请求数据的示例:

定义你的验证规则

use axum::debug_handler;
use loco_rs::prelude::*;
use serde::Deserialize;
use validator::Validate;

#[derive(Debug, Deserialize, Validate)]
pub struct DataParams {
    #[validate(length(min = 5, message = "custom message"))]
    pub name: String,
    #[validate(email)]
    pub email: String,
}

创建带有验证的 Handler

use axum::debug_handler;
use loco_rs::prelude::*;

#[debug_handler]
pub async fn index(
    State(_ctx): State<AppContext>,
    JsonValidate(params): JsonValidate<DataParams>,
) -> Result<Response> {
    format::empty()
}

使用 JsonValidate 提取器,Loco 自动对 DataParams 结构执行验证:

  • 如果验证通过,handler 将继续使用 params 执行。
  • 如果验证失败,则返回 400 Bad Request 响应。

以 JSON 格式返回验证错误

如果你想以结构化的 JSON 格式返回验证错误,请使用 JsonValidateWithMessage 而不是 JsonValidate。响应格式将如下所示:

{
  "errors": {
    "email": [
      {
        "code": "email",
        "message": null,
        "params": {
          "value": "ad"
        }
      }
    ],
    "name": [
      {
        "code": "length",
        "message": "custom message",
        "params": {
          "min": 5,
          "value": "d"
        }
      }
    ]
  }
}

分页

在许多场景中,当查询数据并向用户返回响应时,分页至关重要。在 Loco 中,我们提供了一种直接的方法来分页你的数据,并为你的 API 响应保持一致的分页响应模式。

我们假设你有一个 notes 实体和/或 scaffold(用你喜欢的任何实体替换它)。

使用分页

use loco_rs::prelude::*;

let res = query::fetch_page(&ctx.db, notes::Entity::find(), &query::PaginationQuery::page(2)).await;

使用带过滤器的分页

use loco_rs::prelude::*;

let pagination_query = query::PaginationQuery {
    page_size: 100,
    page: 1,
};

let condition = query::condition().contains(notes::Column::Title, "loco");
let paginated_notes = query::paginate(
    &ctx.db,
    notes::Entity::find(),
    Some(condition.build()),
    &pagination_query,
)
.await?;
  • 首先定义你要检索的实体。
  • 创建你的查询条件(在本例中,过滤标题列中包含“loco”的行)。
  • 定义分页参数。
  • 调用 paginate 函数。

分页视图

在前面的示例中创建并获取 paginated_notes 后,你可以选择要从模型返回哪些字段,并在所有不同的数据响应中保持相同的分页响应。

在 Loco 视图中定义你返回给用户的数据。如果你不熟悉视图,请参阅文档以获取更多上下文。

src/view/notes 中创建一个 notes 视图文件,其中包含以下代码:

use loco_rs::{
    controller::views::pagination::{Pager, PagerMeta},
    prelude::model::query::PaginatedResponse,
};
use serde::{Deserialize, Serialize};

use crate::models::_entities::notes;

#[derive(Debug, Deserialize, Serialize)]
pub struct ListResponse {
    id: i32,
    title: Option<String>,
    content: Option<String>,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct PaginationResponse {}

impl From<notes::Model> for ListResponse {
    fn from(note: notes::Model) -> Self {
        Self {
            id: note.id.clone(),
            title: note.title.clone(),
            content: note.content,
        }
    }
}

impl PaginationResponse {
    #[must_use]
    pub fn response(data: PaginatedResponse<notes::Model>, pagination_query: &PaginationQuery) -> Pager<Vec<ListResponse>> {
        Pager {
            results: data
                .page
                .into_iter()
                .map(ListResponse::from)
                .collect::<Vec<ListResponse>>(),
            info: PagerMeta {
                page: pagination_query.page,
                page_size: pagination_query.page_size,
                total_pages: data.total_pages,
            },
        }
    }
}

测试

当测试 controller 时,目标是调用路由的 controller 端点并验证 HTTP 响应,包括状态代码、响应内容、标头等。

要初始化测试请求,请使用 use loco_rs::testing::prelude::*;,它会准备你的应用程序路由器,提供请求实例和应用程序上下文。

在以下示例中,我们有一个 POST 端点,它返回在 POST 请求中发送的数据。

use loco_rs::testing::prelude::*;

#[tokio::test]
#[serial]
async fn can_print_echo() {
    configure_insta!();

    request::<App, _, _>(|request, _ctx| async move {
        let response = request
            .post("/example")
            .json(&serde_json::json!({"site": "Loco"}))
            .await;

        assert_debug_snapshot!((response.status_code(), response.text()));
    })
    .await;
}

正如你所见,初始化测试请求并使用 request 实例调用 /example 端点。 请求返回一个 Response 实例,其中包含状态代码和响应测试。