LoginSignup
5
1

More than 1 year has passed since last update.

RustでOpenAPIのYamlからAxumのコードを自動生成してみた

Posted at

この記事は MIXI DEVELOPERS Advent Calendar 2022 16 日目の記事です。

前書き

RustのAxumでAPIの開発をしていてOpenAPIからのコード生成できる方法が見つからなくてなんとかできないかと思い試してみました。
コードは以下リポジトリにあります。
https://github.com/Taillook/oapi-rustgen

処理の流れ

(エラー処理とか一旦考えてないのでunwrap気軽に使ってます)

Yamlの読み込み

Yamlの読み込みはyaml_rustを使って読み込みます

use yaml_rust::{Yaml, YamlLoader};

fn load_yaml(path: &str) -> Vec<Yaml> {
    let f = fs::read_to_string(path);
    let s = f.unwrap().to_owned();
    let docs = YamlLoader::load_from_str(&s).unwrap();
    docs
}

読み込むYamlは以下のような形式です。

openapi: 3.0.2
info:
  title: todo
  version: 0.0.1
servers:
- url: http://localhost:3000
paths:
  /todo:
    get:
      description: todo list
      operationId: getTodo
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TodoResponse'
          description: 200 response
      tags:
      - todos
components:
  schemas:
    TodoResponse:
      properties:
        todoList:
          type: array
          items:
            $ref: '#/components/schemas/Todo'
      required:
      - todoList
    Todo:
      example:
        id: 1
        todo: "MTG 11:00-"
      properties:
        id:
          type: integer
        todo:
          type: string
      required:
      - id
      - todo

コードの生成

ここではAxumのRouter部分の生成を目標に生成コードを書きます。
コードの生成にはcodegenを使います。
pathsのHashを回して各pathごとにmethodとoperationIdを取り出し、生成コード用のフォーマットで文字列を生成しています。
ここで作った文字列をscope.new_fn("router")の関数内の処理として追加します。

use std::{
    fs::{self, File},
    io::Write,
};
use codegen::Scope;
use convert_case::{Case, Casing};

fn generate(spec: String, output: String) {
    let mut scope = Scope::new();
    let mut line: String = "".to_owned();

    let docs = load_yaml(&spec.as_str());
    let doc = docs.first().unwrap();

    for path in doc["paths"].as_hash().unwrap() {
        let mut methods: String = "".to_owned();

        for method in path.1.as_hash().unwrap() {
            if methods.len() == 0 {
                methods = format!(
                    "{}(handler::{})",
                    method.0.as_str().unwrap(),
                    method.1["operationId"]
                        .as_str()
                        .unwrap()
                        .to_case(Case::Snake)
                );
            } else {
                methods = format!(
                    "{}.{}(handler::{})",
                    methods,
                    method.0.as_str().unwrap(),
                    method.1["operationId"]
                        .as_str()
                        .unwrap()
                        .to_case(Case::Snake)
                );
            }
        }

        line = format!(
            "{}.route(\"{}\", {})",
            line,
            path.0.as_str().unwrap(),
            methods
        );
    }

    scope
        .new_fn("router")
        .vis("pub")
        .ret("axum::Router")
        .line(format!("axum::Router::new(){}", line));

    let mut file = File::create(output).unwrap();
    match write!(file, "{}", scope.to_string()) {
        Ok(_) => match file.flush() {
            Ok(_) => (),
            Err(e) => println!("{:?}", e),
        },
        Err(e) => println!("{:?}", e),
    }
}

最終的に生成されたコードが以下になります。

pub fn router() -> axum::Router {
    axum::Router::new().route("/todo", get(handler::get_todo).post(handler::post_todo))
}

まとめ

Axumを使うときに便利になりそうなコード生成のツールを作ってみました。
いい感じのやり方知ってる方いればコメントいただきたいです。以上。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1