9个版本
0.6.1 | 2024年8月4日 |
---|---|
0.6.0 | 2024年8月4日 |
0.5.3 | 2024年8月3日 |
0.4.2 | 2024年7月31日 |
#1984 在 过程宏 中
每月下载量 729
24KB
577 行
gluer
一个用于消除前端和后端之间冗余类型和函数定义的Rust框架包装器。目前,它仅支持 axum
框架。
命名来源
"gluer" 这个名字灵感来源于工具的主要功能,即 将基于Rust的Web应用程序的不同部分粘合在一起。正如胶水将不同的材料粘合在一起形成一个整体一样,gluer
将前端和后端的各个组件集成在一起,确保它们无缝工作且没有冗余代码。
安装
将以下内容添加到您的 Cargo.toml
[dependencies]
gluer = "0.7.2"
功能
- 根据 使用说明 中概述的定义路由和API生成。
- 所有操作都在宏展开(编译时间)上完成,甚至包括生成TypeScript文件。
- 完全支持
axum
的类型。 - 使用以下内容生成TypeScript文件
- 自定义基本URL
- 访问API的函数,推断输入和输出类型
- 结构体作为接口,支持通过
#[meta(...)]
属性更改生成的类型 - 枚举作为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)]`
#[meta(into = String)]
user: User,
#[meta(skip)]
borrower: User,
}
// 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: 添加路由
使用route!
宏结合axum
的Router来添加路由。这将启用generate!
宏识别路由并生成相应的函数、结构体、类型和枚举。注意,内联函数不能使用,因为生成的TypeScript文件中的函数名是从处理程序函数名推断出来的。
use axum::{
routing::get,
Json,
Router,
extract::Path,
};
use gluer::{route, 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<()> = Router::new()
// Add non-API-important directly on the router
.route("/", get(root));
// Add API-important routes with the route macro
route!(app, "/:hello", get(hello));
步骤 3: 生成API
使用generate!
宏生成API文件。此宏在宏展开(编译时间)期间生成TypeScript文件。您需要指定当前项目的project_paths
,可以是根目录(表示为"src"
),多个目录或特定文件(例如,["dir0", "dir1", "dir2/some.rs"]
)。将扫描project_paths
以检索项目数据,即收集由route!
和#[metadata]
宏标记的信息。此外,您还需要提供一个将生成的文件保存到的path
,包括文件名,以及API的base
URL。基本URL不应以斜杠结束(/
);如果您正在使用axum
的静态文件服务,请使用""
表示没有基本URL,或者提供类似"https://127.0.0.1:8080"
的URL以用于本地服务器。
use gluer::generate;
// Make sure to change "tests" to "src" when copying this example into a normal project
generate!("tests", "tests/api.ts", "");
现在您只需简单地使用路由器启动服务器或进行其他操作,API应由您的LSP生成!
完整示例
以下是使用gluer
与axum
的完整示例
use axum::{
extract::{Path, Query},
routing::get,
Json, Router,
};
use gluer::{generate, metadata, route};
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
age: AgeInner,
}
#[metadata]
#[derive(Serialize, Deserialize, Default)]
struct AgeInner {
age: u8,
}
#[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 mut _app: Router = Router::new();
route!(_app, "/:p", get(fetch_root).post(add_root));
route!(_app, "/char/:path/metadata/:path", get(get_alphabet));
// Make sure to change "tests" to "src" when copying this example into a normal project
generate!("tests", "tests/api.ts", "");
let _listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
.await
.unwrap();
// starts the server, comment in and rename `_listener` to run it
// axum::serve(listener, app).await.unwrap();
}
依赖项
~285–740KB
~18K SLoC