この記事は 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を使うときに便利になりそうなコード生成のツールを作ってみました。
いい感じのやり方知ってる方いればコメントいただきたいです。以上。