Rust中使用Salvo自动生成API服务、TypeScript和Dart


该项目会生成从 Rust 后端到 TypeScript 和 Dart 客户端的 API 服务。

问题描述:
在跨 API 的团队中工作时,客户端和服务之间的对象、类型和类保持准确非常重要。
我从 Rest API 调用中发现的类型不正确、属性缺失等错误数量令人震惊。
我们研究了 GraphQL、tRPC、Rust Type Convert、gRPC 等多种技术,以确定哪种技术适合我们的需求。不幸的是,他们缺少自动客户端转换的一些重要功能,例如缺少错误类型、缺少返回类型、一切都是可选的(GraphQL ..)
在尝试了许多不同的 API 后,我们偶然发现了 Rust 的 Salvo。

使用 Salvo,您可以自动生成整个 Swagger 架构,其中包括返回类型、错误枚举等。
您无需执行任何手动工作,例如手动指定 OpenAPI 属性。它将结构转换为规格。

设置项目:
首先,在终端内创建一个项目目录

mkdir ./my-auto-gen-project
cd my-auto-gen-project

创建我们的 Rust 项目

cargo new my_api
cd my_api
// Open in VSC code (optional)
code .

将以下依赖内容添加到Cargo.toml

[dependencies]
salvo = { version = "*", features = ["oapi", "cors"] }
tokio = { version =
"1", features = ["macros"] }
tracing =
"0.1.40"
tracing-subscriber =
"0.3"

编码
打开src/main.rs并添加以下内容,这将设置我们的 API 和 UI

use salvo::cors::Cors;

use salvo::hyper::Method;

use salvo::oapi::extract::*;

use salvo::prelude::*;

#[endpoint]

async fn hello(name: QueryParam<String, false>) -> String {

    format!("Hello, {}!", name.as_deref().unwrap_or("World"))

}

#[tokio::main]

async fn main() {

    tracing_subscriber::fmt().init();

    let router = Router::new().push(Router::with_path(
"hello").get(hello));

    let doc = OpenApi::new(
"test api", "0.0.1").merge_router(&router);

   
// Allow requests from origin

    let cors = Cors::new()

        .allow_origin(
"*")

        .allow_methods(vec![Method::GET, Method::POST, Method::DELETE])

        .into_handler();

    let router = router

        .push(doc.into_router(
"/api-doc/openapi.json"))

        .push(SwaggerUi::new(
"/api-doc/openapi.json").into_router("ui"))

        .hoop(cors);

    let acceptor = TcpListener::new(
"127.0.0.1:5800").bind().await;

    Server::new(acceptor).serve(router).await;

}

运行
现在在编辑器中打开终端并写入
cargo run

您应该能够访问http://127.0.0.1:5800/ui/
您将看到 Swagger UI

感谢 Salvo,我们的端点是自动生成的

正如您所看到的,我们的 API、文档和 Playground 完全是由上面一小段代码生成的,这真是太神奇了。

我们将使用它作为 TypeScript 和 Dart (Flutter) 项目的基础来自动生成我们的 API。

前端TypeScript客户端
现在我们已经完成了基本的 API 设置和 swagger 文档,接下来让我们从 TypeScript 开始生成前端客户端。

对于虚拟框架,我们将使用 SvelteKit,因为我们认为这是最短的工作量。
使用哪种框架(React、Vue 等)并不重要,它们都会起作用,因为我们正在生成可在任何 TypeScript 项目上使用的 API 服务(SSR 可能有一些修改)

生成 Svelte-kit 项目

// Svelte project

npm create svelte@latest web-frontend

(select SvelteKit demo app, Typescript, ESLint)

cd web-frontend

npm install

// Optional, we're opening in our editor VSC

code .

// Check that the project runs

npm run dev

//打开浏览器,您应该会看到一个 SvelteKit 示例项目。
http:
//localhost:5173/

生成有趣的 API 
打开Svelte 项目中的package.json 。我们将添加以下“script”行,并将其命名为“gen-api”

// 您可以定义不同类型的请求者,例如 typescript-[client|axios|fetch]
npx @openapitools/openapi-generator-cli generate -i http:
//127.0.0.1:5800/api-doc/openapi.json -g typescript-fetch -o ./src/api/

还有其他选项,例如基本路径等。它应该如下所示:

"scripts": {

        
"dev": "vite dev",

        
"build": "vite build",

        
"gen-api": "npx @openapitools/openapi-generator-cli generate -i http://127.0.0.1:5800/api-doc/openapi.json -g typescript-fetch -o ./src/api/",

        
"preview": "vite preview",

        
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",

        
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"

    },

现在,如果我们想生成 API,我们只需运行

npm run gen-api 

您应该会在 SRC 下看到生成的 api目录

让我们将 API 添加到页面并进行测试
转到src/routes/+page.svelte
将页面代码更改为如下所示:

<script>

import { Configuration, DefaultApi } from "../api";

import { onMount } from
"svelte";

let test =
"this is a test";

onMount(async () => {

    console.log(
"onMount");

    const config = new Configuration({

    basePath:
"http://localhost:5800",

    });

    const api = new DefaultApi(config);

    test = await api.myApiHello({ name:
"Olly" });

});

</script>

<svelte:head>

<title>Home</title>

<meta name=
"description" content="Svelte demo app" />

</svelte:head>

<section>

<h1>

    {test}

</h1>

</section>

<style>

section {

    display: flex;

    flex-direction: column;

    justify-content: center;

    align-items: center;

    flex: 0.6;

}

h1 {

    width: 100%;

}

</style>
            

浏览器打开http://localhost:5173
这里我们使用自动生成的 TypeScript API。它甚至带有自动完成、参数、错误类型和输入/输出。
API 100% 准确,不再需要手动编写服务、轻松更新等等。

现在是 Flutter 应用程序和 Dart 代码
这也适用于外部服务的 Dart 后端。

我们尝试了一些 Dart 自动生成工具,但不幸的是,其中很多都被破坏或遗漏了 API 的关键部分(如返回类型!)。

最后,我们使用了一个名为 "swagger_dart_code_generator "的工具。

它似乎能完全工作,也就是说,它能生成类型、枚举、复杂对象等。实际上,官方的 swagger cli 在复杂对象方面是有问题的,所以我们不得不使用这个

再次进入项目根目录,生成 flutter 应用程序

flutter create mobile_app
cd mobile_app
// Optional opening the project
code .

打开 pubspec.yaml 文件

确保它看起来像这样

name: mobile_app
description: "My auto-gen project"

# pub.dev using 'flutter pub publish'. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 1.0.0+1

environment:
    sdk: '>=3.2.3 <4.0.0'

dependencies:
    flutter:
    sdk: flutter
    chopper: ^7.0.10
    json_annotation: ^4.8.0
    swagger_dart_code_generator: ^2.14.2

    cupertino_icons: ^1.0.2

dev_dependencies:
    flutter_test:
    sdk: flutter
    build_runner: ^2.3.3
    chopper_generator: ^7.0.7
    json_serializable: ^6.6.1
    flutter_lints: ^3.0.1

flutter:
    uses-material-design: true

在移动项目根目录下创建 build.yaml,确保如下所示

targets:
$default:
    sources:
    - lib/**
    - open_api/**
    builders:
    swagger_dart_code_generator:
        # https://pub.dev/packages/swagger_dart_code_generator
        options:
        input_folder: "open_api/"
        output_folder: "lib/generated_api/"
        add_base_path_to_requests: true
        input_urls: 
            - url: "http://127.0.0.1:5800/api-doc/openapi.json"

它的作用是连接到 swagger 规范,并生成 dart 代码。

在应用程序项目根目录下创建一个 "open_api "文件夹

现在在终端运行
dart run build_runner build

发生两件事

  • 1.应该已经下载了 openapi.json 规范
  • 2.应已从中创建了生成的 API。

设置完成后,您就可以像使用 TypeScript 项目一样使用 API 了。

转到 main.dart

我们将修改默认的计数器程序。当你按下 + 按钮时,它会发出请求并填写文本。

import 'package:flutter/material.dart';
import 'package:mobile_app/generated_api/client_index.dart';

void main() {
    runApp(const MyApp());
}

class MyApp extends StatelessWidget {
    const MyApp({super.key});

    @override
    Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
        ),
        home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
    }
}

class MyHomePage extends StatefulWidget {
    const MyHomePage({super.key, required this.title});

    final String title;

    @override
    State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
    String _text = "";

    void _incrementCounter() {
    _test();
    }

    @override
    void initState() {
    super.initState();
    }

    _test() async {
    print("test");

    final Openapi api = Openapi.create(
        baseUrl: Uri.parse("http://localhost:5800"),
    );

    try {
        final data1 = await api.helloGet(name: "Olly");
        setState(() {
        _text = data1.body!; // << Is string, or can be complex object.
        });
    } catch (e) {
        print(e);
    }
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
        ),
        body: Center(
        child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
            Text(_text),
            ],
        ),
        ),
        floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
        ),
    );
    }
}

点击 "加号 "按钮,它就会跳转到 API,然后再返回。你甚至可以在这里使用复杂类型。Rust API 还将支持复杂类型返回、验证等功能。我们的完整版还支持错误枚举和状态代码字符串,这样开发人员就能清楚地知道该如何处理。

Github 演示项目链接