4个版本 (重大更改)

0.9.0 2024年7月2日
0.8.0 2024年6月5日
0.7.0 2024年5月30日
0.6.0 2024年4月2日

#4 in #tide

Download history 479/week @ 2024-04-14 663/week @ 2024-04-21 689/week @ 2024-04-28 639/week @ 2024-05-05 331/week @ 2024-05-12 479/week @ 2024-05-19 980/week @ 2024-05-26 813/week @ 2024-06-02 473/week @ 2024-06-09 358/week @ 2024-06-16 517/week @ 2024-06-23 477/week @ 2024-06-30 413/week @ 2024-07-07 344/week @ 2024-07-14 679/week @ 2024-07-21 586/week @ 2024-07-28

2,030 每月下载量
用于 surf-disco

自定义许可

300KB
5.5K SLoC

Tide Disco

Tide提供可发现性支持

我们说一个系统是可发现的,如果对使用方面的猜测和错误能得到相关的文档和协助,以生成正确的请求。为了以实用的方式提供这种功能,最好在数据文件中指定API,而不是在代码中,这样就可以在一个简洁易读的规范中编辑所有相关文本。

利用TOML来指定

  • 带类型参数的路由
  • 路由文档
  • 路由错误消息
  • 一般文档

目标

  • 上下文相关帮助
  • 拼写建议
  • 从路由文档汇编的参考文档
  • 表单和其他用户界面,以帮助构建正确的输入
  • 本地化
  • 新手和专家帮助
  • 灵活的路由解析,例如命名参数而不是位置参数
  • 基于参数类型的API模糊测试自动化

未来

WebSocket支持 运行时日志控制


lib.rs:

Tide Disco是一个内置了Tide可发现性支持的Web服务器框架

概述

我们说一个系统是可发现的,如果对使用方面的猜测和错误能得到相关的文档和协助,以生成正确的请求。为了以实用的方式提供这种功能,最好在数据文件中指定API,而不是在代码中,这样就可以在一个简洁易读的规范中编辑所有相关文本。

Tide Disco利用TOML来指定

  • 带类型参数的路由
  • 路由文档
  • 路由错误消息
  • 一般文档

目标

  • 上下文相关帮助
  • 拼写建议
  • 从路由文档汇编的参考文档
  • 表单和其他用户界面,以帮助构建正确的输入
  • 本地化
  • 新手和专家帮助
  • 灵活的路由解析,例如命名参数而不是位置参数
  • 基于参数类型的API模糊测试自动化

未来工作

  • WebSocket支持
  • 运行时日志控制

入门

一个Tide Disco应用程序由一个或多个API模块组成。API模块由一个TOML规范和一组路由处理器(Rust函数)组成,以提供TOML中定义的路由的行为。你可以通过查看这个crate中的示例来了解TOML文件的格式。一旦你有了它,你就可以使用Api::new将其加载到API描述中。

use tide_disco::Api;
use tide_disco::error::ServerError;
use vbs::version::StaticVersion;

type State = ();
type Error = ServerError;
type StaticVer01 = StaticVersion<0, 1>;

let spec: toml::Value = toml::from_str(
    std::str::from_utf8(&std::fs::read("/path/to/api.toml").unwrap()).unwrap(),
).unwrap();
let mut api = Api::<State, Error, StaticVer01>::new(spec)?;

一旦你有了[Api],你就可以为你TOML规范中的任何路由定义路由处理器。

[route.hello]
PATH = ["hello"]
METHOD = "GET"

注册处理器的示例:

use futures::FutureExt;

api.get("hello", |req, state| async move { Ok("Hello, world!") }.boxed())?;

有关如何创建[Api]的详细信息,请参阅API参考

一旦注册了所有路由处理器,你需要将你的[Api]模块注册到[App]。

use tide_disco::App;
use vbs::version::{StaticVersion, StaticVersionType};

type StaticVer01 = StaticVersion<0, 1>;

let mut app = App::<State, Error>::with_state(());
app.register_module("api", api);
app.serve("http://localhost:8080", StaticVer01::instance()).await;

然后您可以使用您的应用程序

curl http://localhost:8080/api/hello

期货合约

作为一个网络服务器框架,Tide Disco自然包括许多接受函数作为参数的接口。例如,通过传递一个处理函数到[Api]对象来注册路由处理程序。而且自然地,这些函数参数中的许多都是异步的,这当然只是意味着它们是返回某些类型 F 的常规函数,这些函数实现了Future特质。这都很正常,但在这个crate的接口中,您可能会注意到一些有点不寻常的地方:许多这些函数不仅需要返回任何Future,而且需要返回一个BoxFuture。这是由于当前Rust编译器存在的一个限制。

问题出现在返回的未来不是'static,而是从函数参数中借用的函数上。例如,考虑以下路由定义

type State = RwLock<u64>;
type Error = ();

api.at("someroute", |_req, state: &State| async {
    Ok(*state.read().await)
})

路由处理程序中的async块使用了state引用,因此生成的未来仅在引用state有效的时间内有效。我们可以这样编写路由处理程序的签名

use futures::Future;
use tide_disco::RequestParams;

type State = async_std::sync::RwLock<u64>;
type Error = ();

fn handler<'a>(
    req: RequestParams,
    state: &'a State,
) -> impl 'a + Future<Output = Result<u64, Error>> {
    // ...
    # async { Ok(*state.read().await) }
}

注意我们如何使用impl语法显式地通过生命周期'a约束未来类型。不幸的是,虽然我们可以编写这样的函数签名,但我们不能编写使用[Fn]特质并代表等效函数签名的类型约束。这是一个问题,因为像at这样的接口希望消费任何实现了[Fn]的功能对象,而不仅仅是静态函数指针。这是我们编写的

impl<State, Error, VER: StaticVersionType> Api<State, Error, VER> {
    pub fn at<F, T>(&mut self, route: &str, handler: F)
    where
        F: for<'a> Fn<(RequestParams, &'a State)>,
        for<'a> <F as Fn<(RequestParams, &'a State)>>::Output:
            'a + Future<Output = Result<T, Error>>,
    {...}
}

在这里,我们使用关联类型Output的更高阶特质约束来约束F的[Fn]实现的输出,从而通过State引用的生命周期来约束未来。实际上,您可以在不稳定Rust(使用原始[Fn]特质作为约束是不稳定的)中编写这个函数签名,但即使这样,由于编译器中的一个错误,没有任何关联类型能够实现HRTB。这个限制在这篇帖子中有详细描述。

作为解决这个问题的工作区,我们要求函数F返回一个具有显式生命周期参数的具体未来类型:BoxFuture。这允许我们在F本身上指定HRTB的生命周期约束,而不是求助于关联类型Output上的单独HRTB,以便能够命名F的返回类型。以下是at的实际(部分)签名

impl<State, Error, VER: StaticVersionType> Api<State, Error, VER> {
    pub fn at<F, T>(&mut self, route: &str, handler: F)
    where
        F: for<'a> Fn(RequestParams, &'a State) -> BoxFuture<'a, Result<T, Error>>,
    {...}
}

这意味着您传递给Tide Disco框架的函数必须返回一个boxed未来。当传递闭包时,您只需在您的async块中添加.boxed()即可,如下所示

use async_std::sync::RwLock;
use futures::FutureExt;
use tide_disco::Api;
use vbs::version::StaticVersion;

type State = RwLock<u64>;
type Error = ();

type StaticVer01 = StaticVersion<0, 1>;

fn define_routes(api: &mut Api<State, Error, StaticVer01>) {
    api.at("someroute", |_req, state: &State| async {
        Ok(*state.read().await)
    }.boxed());
}

这也意味着您不能直接传递async fn的名称,因为使用async fn语法声明的异步函数不返回boxed未来。相反,您可以将函数包装在一个闭包中

use async_std::sync::RwLock;
use futures::FutureExt;
use tide_disco::{Api, RequestParams};
use vbs::version::StaticVersion;

type State = RwLock<u64>;
type Error = ();
type StaticVer01 = StaticVersion<0, 1>;

async fn handler(_req: RequestParams, state: &State) -> Result<u64, Error> {
    Ok(*state.read().await)
}

fn register(api: &mut Api<State, Error, StaticVer01>) {
    api.at("someroute", |req, state: &State| handler(req, state).boxed());
}

未来,我们可能创建一个属性宏,它可以直接将 async fn 重写为返回一个boxed future,例如

#[boxed_future]
async fn handler(_req: RequestParams, state: &State) -> Result<u64, Error> {
    Ok(*state.read().await)
}

依赖项

~26–42MB
~694K SLoC