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
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