Loco + utoipaでenumを整数として扱う
TL;DR
PartialSchema
とToSchema
を自前で実装すればOK
はじめに
LocoはRuby on RailsのRust版のようなものです。本稿執筆時点の最新バージョンは0.11.0でありまだv1になっていませんが、活発な開発がされています。
utoipaはOpenAPIのドキュメンテーションを自動生成するクレートです。今回はLocoのプロジェクトでswagger.jsonの生成に使用します。utopiaではなくutoipaなので注意してください。
Loco公式によるutoipa使用例が既にあるのですが、これだけでは物足りません。表題にもあるように、
- DBではsmallintで扱われている
- model層でenumの皮を被っている
- レスポンスにシリアライズする時はもとのsmallintの値を使い、swagger定義にもこれを反映したい
というケースについて解説します。
レスポンスで整数にシリアライズする
model層でenumとして扱う方法はSeaORMの公式docsを参考に、最終的に整数にシリアライズする方法はserdeの公式docsを参考にすれば簡単です。
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use utoipa::ToSchema;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, ToSchema)]
#[sea_orm(table_name = "items")]
#[schema(as = Item)]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
// 中略
#[schema(inline)]
pub condition: Condition,
}
#[derive(EnumIter, DeriveActiveEnum, Clone, Copy, Debug, PartialEq, Eq, Deserialize_repr, Serialize_repr, ToSchema)]
#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
#[repr(i16)]
pub enum Condition {
New = 1,
Good = 2,
Damaged = 3,
}
swagger定義を整数にする
上記のままutoipaを用いてswagger.jsonを生成すると以下のようになります。実際に吐き出されるJSONではcondition
の値は整数になるので、このswagger定義は嘘になってしまいます。
{
// 中略
"components": {
"schemas": {
"Item": {
"type": "object",
"required": [
"id",
// 中略
"condition"
],
"properties": {
"id": {
"type": "integer",
"format": "int32"
},
// 中略
"condition": {
"type": "string",
"enum": [
"New",
"Good",
"Damaged"
]
}
}
}
}
}
}
そこで、以下のようにToSchema
トレイトを自前で実装します。
use utoipa::{openapi::{RefOr, Schema}, PartialSchema, ToSchema};
// 中略
#[derive(EnumIter, DeriveActiveEnum, Clone, Copy, Debug, PartialEq, Eq, Deserialize_repr, Serialize_repr)]
#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
#[repr(i16)]
pub enum Condition {
New = 1,
Good = 2,
Damaged = 3,
}
impl PartialSchema for Condition {
fn schema() -> RefOr<Schema> {
i16::schema()
}
}
impl ToSchema for Condition {}
これでswagger定義が正しくなります。
"condition": {
"type": "integer",
"format": "int32"
}
マクロを作成して記述を減らす
とはいえ、enumを作るたびにいちいちPartialSchema
とToSchema
を実装するのは面倒です。そこで、ToSchema_repr
トレイトを用意して以下のように書けるようにします。ToSchema_repr
トレイトをderiveすると#[repr(i16)]
の部分を読み、その型でswaggerを生成します。
use utoipa::ToSchema;
use utoipa_repr::ToSchema_repr;
// 中略
#[derive(EnumIter, DeriveActiveEnum, Clone, Copy, Debug, PartialEq, Eq, Deserialize_repr, Serialize_repr, ToSchema_repr)]
#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
#[repr(i16)]
pub enum Condition {
New = 1,
Good = 2,
Damaged = 3,
}
serde_reprのようにライブラリを作って公開するのが筋だとは思いますが、面倒なのでここではLocoのプロジェクト内で済ませる方法を紹介します。
まず、適当にワークスペースを作成してプロジェクトルートのCargo.tomlに追加します。
[dependencies]
+ utoipa_repr = { path = "utoipa_repr" }
[package]
name = "utoipa_repr"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "utoipa_repr"
path = "src/lib.rs"
proc-macro = true # ←ここ重要
[dependencies]
proc-macro2 = "1.0.88"
quote = "1.0.37"
syn = "2.0.79"
次に、serde_reprのparse.rsをutoipa_repr/src/parse.rs
にコピペします(欲しい構造体が公開されていないため、use serde_repr::*;
では取り出せません)。
lib.rs
は以下のようにします。
mod parse;
use parse::Input;
use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;
#[proc_macro_derive(ToSchema_repr)]
pub fn derive(input: TokenStream) -> TokenStream {
// identには`Condition`のようなenumの名前が、reprには`i16`のような型が入る
let Input { ident, repr, .. } = parse_macro_input!(input as Input);
// さっき書いたやつのenum名と型名をプレースホルダにする
TokenStream::from(quote! {
impl utoipa::PartialSchema for #ident {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::Schema> {
#repr::schema()
}
}
impl utoipa::ToSchema for #ident {}
})
}
これにより、ToSchema
トレイトの代わりにToSchema_repr
トレイトをderiveすることで正しいswagger定義を出力させられます。
最後に
Loco + utoipaでenumを整数にシリアライズする方法を紹介しました。さらに、(ライセンス問題をクリアする必要がありますが)マクロ化して記述量を減らす方法を紹介しました。
LocoはRustの静的型付けとRailsの恩恵を同時に受けられる面白いフレームワークなので、皆さんも使ってみてください。
Discussion