20个版本 (7个重大更改)
新版本 0.8.2 | 2024年8月17日 |
---|---|
0.7.3 | 2024年8月12日 |
0.4.2 | 2024年7月31日 |
#1706 in Web编程
1,624 每月下载量
66KB
1.5K SLoC
gluer
用于Rust框架的包装器,消除了前后端之间的冗余类型和函数定义。目前,它只支持 axum
框架。
命名来源
“gluer”这个名字灵感来源于工具的主要功能,即 将基于Rust的Web应用程序的不同部分粘合在一起。正如胶水将不同的材料粘合在一起形成一个完整的整体一样,gluer
集成了前后端的各种组件,确保它们无缝工作且没有冗余代码。
安装
将此添加到您的 Cargo.toml
[dependencies]
gluer = "0.8.2"
功能
- 按照如何使用中概述的方式定义路由和API生成。
- 所有操作都在宏展开(编译时间)上完成,甚至包括生成TypeScript文件。
- 完全支持
axum
的类型。 - 使用
- 自定义基本URL
- 访问API的函数,推断输入和输出类型
- 结构体作为接口,支持通过
#[meta(...)]
属性跳过,不同的Rust类型或可选的(TypeScript中的?
) - 枚举作为TypeScript的等效类型,不支持具有值的枚举,因为TypeScript中缺少该功能
- 类型作为TypeScript的等效类型
- 支持将文档字符串转换为TypeScript的等效类型,即使是结构体和枚举的字段
- 元组作为TypeScript的等效类型,也支持在
axum
的路径中的元组 - 支持将Rust特定类型
Result
转换为自定义类型,使用custom = [Type, *]
属性 - 泛型,包括多个和嵌套的,可以在这里找到
- 没有额外依赖
如何使用
gluer
会生成一个API端点.ts
文件。要使用它,请按照以下步骤操作
步骤 1:定义结构体和函数
要定义您的结构体、函数和枚举,请使用#[metadata]
宏以及#[meta(...)]
属性。这可以使generate!
宏找到这些内容并将它们转换为TypeScript等效内容。
use axum::{
Json,
extract::Path,
};
use gluer::metadata;
use serde::{Serialize, Deserialize};
/// Define a struct with the metadata macro
/// Note: This is a docstring and will be
/// converted to the TypeScript equivalent
#[metadata(custom = [Result])]
#[derive(Serialize, Deserialize)]
struct Book {
/// This will also be converted to a docstring
name: String,
// When you use types as `Result`, `Option` or `Vec` the
// macro sees them as a default rust type, meaning when
// you wanting to use custom ones you have to specify that
// via the `custom` attribute on `#[metadata]`
some_result: Result<String>,
// Sometimes you don't have access to certain data types,
// so you can override them using `#[meta(into = Type)]`
// or skip them entirely via `#[meta(skip)]`
// or set them to be optional via `#[meta(optional)]`
// (-> an interface field with a `?`)
#[meta(into = String)]
user: User,
#[meta(skip)]
borrower: User,
#[meta(optional)]
reservation: String,
}
// Everything you want to use, even if it's just a
// dependency of struct or type, needs to be declared
// with the `#[metadata]` macro
#[metadata]
type Result<T> = std::result::Result<T, String>;
#[derive(Default, Serialize, Deserialize)]
struct User {
name: String,
password: String,
}
// Define an enum with the metadata macro
// Note: Enums with values are not supported
#[metadata]
#[derive(Default, Serialize, Deserialize)]
enum BookState {
#[default]
None,
Reserved,
Borrowed,
}
// Define the functions with the metadata macro
#[metadata]
async fn root() -> Json<String> {
"Hello, World!".to_string().into()
}
// Supports axum's input types
#[metadata]
async fn book(Json(b): Json<Book>) -> Json<Book> {
Json(b)
}
// Also tuples in paths
#[metadata]
async fn path(Path(p): Path<(String, String)>) -> Json<(String, String)> {
p.into()
}
// Supports enums
#[metadata]
async fn book_state() -> Json<BookState> {
BookState::default().into()
}
步骤 2:添加路由并生成API
使用generate!
宏来定义您的路由和其他遥测数据以生成API。您必须定义TypeScript文件的output
位置和routes
。请注意,由于生成的TypeScript文件中的函数名称是从处理程序函数名称推断出来的,因此不能在router
字段中使用内联函数。
use axum::{
routing::get,
Json,
Router,
extract::Path,
};
use gluer::{generate, metadata};
// without `#[metadata]`, it's non-API-important
async fn root() -> String {
"Hello, World!".to_string()
}
// done like above
#[metadata]
async fn hello(Path(h): Path<String>) -> Json<String> {
h.into()
}
let mut app: Router<()> = generate! {
routes = {
// Add API-important inside the routes field
"hello" = get(hello),
},
output = "tests/api.ts",
}
// Add non-API-important outside the macro
.route("/", get(root));
其他注意事项
generate!
宏包含几个可选字段,您可以根据需要自定义它们。其中之一是prefix
,允许您为API路由设置URL前缀。默认情况下,这是一个空字符串(""
),但您可以将其更改为类似"/api"
的内容。请注意,前缀不应以/
结尾。
另一个可自定义的选项是files
,它定义了包含处理程序函数和依赖项源代码的Rust项目文件。这可以是一个单字符串字面量(例如,"src"
)或字符串字面量的数组(例如,["src/db", "src", "src/error.rs"]
)。这些路径用于从TypeScript客户端提取类型信息。默认值为"src"
,这应该适用于大多数场景。
最后,一旦宏生成了路由以及API,您就可以立即使用它来启动服务器或执行其他操作。
完整示例
以下是一个完整示例,展示了如何使用gluer
与axum
结合使用,或者查看这个项目,它使用了gluer
use axum::{
extract::{Path, Query},
routing::get,
Json, Router,
};
use gluer::{generate, metadata};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// An example of a simple function with a `Path` and `Query` extractor
#[metadata]
async fn fetch_root(Query(test): Query<HashMap<String, String>>, Path(p): Path<usize>) -> String {
test.get(&p.to_string()).unwrap().clone()
}
// Generics are supported, multiple even
// Note: This is not a docstring and won't
// be converted
#[metadata]
#[derive(Serialize, Deserialize, Default)]
pub struct Hello<T: Serialize, S> {
name: S,
vec: Vec<T>,
}
/// Might want to look into the `api.ts` file to see the docstring for this struct
#[metadata]
#[derive(Serialize, Deserialize, Default)]
struct Age {
/// Even supports docstring on fields and optional fields
#[meta(optional)]
age: AgeInner,
}
#[metadata]
#[derive(Serialize, Deserialize, Default)]
struct AgeInner {
// Thats quite big and also supported
/// This gets converted to a `string` on the TypeScript side
/// because `numbers` there cannot be greater than 64 bits
age: u128,
}
#[metadata]
#[derive(Serialize, Deserialize, Default)]
struct Huh<T> {
huh: T,
}
// Even deep nested generics are supported and tagging default rust types as Custom
#[metadata(custom = [Result])]
async fn add_root(
Path(_): Path<usize>,
Json(hello): Json<Result<Hello<Hello<Huh<Huh<Hello<Age, String>>>, String>, String>>>,
) -> Json<Result<String>> {
Json(Ok(hello.unwrap().name.to_string()))
}
#[metadata]
#[derive(Serialize, Deserialize)]
enum Alphabet {
A,
B,
C,
// etc
}
// Even tuples are supported
#[metadata]
async fn get_alphabet(Path(r): Path<(Alphabet, S)>) -> Json<(Alphabet, S)> {
Json(r)
}
/// An example how an api error type could look like
#[metadata]
#[derive(Serialize, Deserialize, Debug)]
enum Error {
/// Normal 404 error
NotFound,
/// Internally something really bad happened
InternalServerError,
}
// And types?!?
#[metadata]
type Result<T> = std::result::Result<T, Error>;
#[metadata]
type S = String;
#[tokio::main]
async fn main() {
let _app: Router<()> = generate! {
routes = { // required
"/:p" = get(fetch_root).post(add_root),
"/char/:path/metadata/:path" = get(get_alphabet),
},
files = "tests", // Make sure to remove this when copying this example into a normal project
output = "tests/api.ts", //required
};
let _listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
.await
.unwrap();
// starts the server, comment in and rename `_app` and `_listener` to run it
// axum::serve(listener, app).await.unwrap();
}
依赖关系
~240–680KB
~16K SLoC