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

この記事は [MIXI DEVELOPERS Advent Calendar 2022](https://qiita.com/advent-calendar/2022/mixi) 16 日目の記事です。

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

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

### Yamlの読み込み
Yamlの読み込みは[yaml_rust](https://docs.rs/yaml-rust)を使って読み込みます

```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は以下のような形式です。
```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](https://docs.rs/codegen)を使います。
pathsのHashを回して各pathごとにmethodとoperationIdを取り出し、生成コード用のフォーマットで文字列を生成しています。
ここで作った文字列を`scope.new_fn("router")`の関数内の処理として追加します。

```rust
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),
    }
}
```

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

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

## まとめ

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