#web-apps #web-framework #generate-typescript #macro #parser #api #api-bindings

gluer

用于Rust框架的包装器,消除了前后端之间的冗余类型和函数定义

20个版本 (7个重大更改)

新版本 0.8.2 2024年8月17日
0.7.3 2024年8月12日
0.4.2 2024年7月31日

#1706 in Web编程

Download history 669/week @ 2024-07-27 704/week @ 2024-08-03 251/week @ 2024-08-10

1,624 每月下载量

MIT 协议

66KB
1.5K SLoC

gluer

crates.io crates.io docs.rs

用于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,您就可以立即使用它来启动服务器或执行其他操作。

完整示例

以下是一个完整示例,展示了如何使用glueraxum结合使用,或者查看这个项目,它使用了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