OpenAPIからRustのクライアントコードを自動生成したい!
はじめに
こんにちは〜!皆様いかがお過ごしでしょうか? no plan inc. CTOの @serinuntius です。
この記事はRust Advent Calendar 2023の2日目の記事です。
今回はOpenAPIの定義書からモックAPIを作成しRustのクライアントコードを自動生成する方法について書いていきます。
OpenAPIの基礎:API仕様の理解
今回はサンプルとしてOpenAPI SpecificationのペットショップのAPI定義書を使用します。
どんなAPIがあるかを確認してみましょう。
/pets
エンドポイント
GET メソッド
- 概要: このメソッドは、登録されている全てのペットのリストを提供します。
-
パラメータ:
limit
(オプション)- 一度に返すペットの最大数を指定します(最大100)。 - レスポンス: 成功すると、ペットの配列と次のページへのリンクを含む200レスポンスを返します。エラーが発生した場合は、エラーの詳細を含むデフォルトレスポンスが返されます。
POST メソッド
- 概要: 新しいペットを作成します。
- レスポンス: ペットの作成に成功すると、201レスポンスが返されます。エラーが発生した場合は、エラーの詳細を含むデフォルトレスポンスが返されます。
/pets/{petId}
エンドポイント
GET メソッド
- 概要: 特定のペットの詳細情報を取得します。
-
パラメータ:
petId
- 取得したいペットのIDを指定します。 - レスポンス: ペットの情報を含む200レスポンスが返されます。ペットが見つからない場合やその他のエラーが発生した場合は、エラーの詳細を含むデフォルトレスポンスが返されます。
コンポーネントスキーマ
このAPI仕様では、Pet
、Pets
、Error
の3つのスキーマが定義されています。Pet
スキーマは個々のペットのデータ構造を定義し、Pets
スキーマはペットの配列を定義します。Error
スキーマはAPIがエラーを返した際のレスポンスの形式を定義します。
環境構築
cargo init openapi-gen
cd openapi-gen
pnpm init
pnpm add -D @openapitools/openapi-generator-cli @stoplight/prism-cli
Prismを使用したモックサーバーの設定
wget https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.yaml
好きなエディタで package.json
を開き、以下のように修正します。
...
"scripts": {
"prism": "prism mock ./petstore.yaml"
}
...
Prismを起動します。
pnpm prism
別窓でcurlを実行してみましょう。
$ curl localhost:4010/pets
[{"id":-9007199254740991,"name":"string","tag":"string"}]%
こんな感じで味気ないペットのデータが返ってきました。
流石にもうちょっといい感じのペットのデータを返して欲しいので、petstore.yaml
を修正します。
components:
schemas:
Pet:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
x-faker: name.firstName # ここを追加
tag:
type: string
x-faker: name.lastName # ここを追加
さらにHeaderに Prefer: dynamic=true
を追加する必要があるようです。
いい感じになりました!
$ curl localhost:4010/pets -H "Prefer: dynamic=true"
[{"id":6434444831111209,"name":"Lorine","tag":"Abshire"},{"id":-6917212062297783,"name":"Gregoria","tag":"Kessler"},{"id":3198439147638889,"name":"Hope","tag":"Paucek"},{"id":-2720091641140911,"name":"Nikki","tag":"Renner"},{"id":-75873064202935,"name":"Dameon","tag":"Kling"},{"id":8821452548480297,"name":"Haylie","tag":"Gerhold"},{"id":1220346173630553,"name":"Dakota","tag":"Lang"},{"id":7943904249935705,"name":"Kole","tag":"Gusikowski"},{"id":6397775688514805,"name":"Sandrine","tag":"Moore"},{"id":-3192328224529287,"name":"Einar","tag":"Boehm"},{"id":-5269481972228867,"name":"Marc","tag":"Mohr"},{"id":-1196169157532435,"name":"Waldo","tag":"Prohaska"},{"id":382402961760605,"name":"Daphney","tag":"Daniel"},{"id":5737578741193737,"name":"Mireya","tag":"Yundt"},{"id":5552614277185193,"name":"Lucy","tag":"Muller"},{"id":5994926263362637,"name":"Elian","tag":"Wisozk"},{"id":-8685276069652907,"name":"Ottis","tag":"Hauck"},{"id":6998548240129661,"name":"Aron","tag":"Graham"},{"id":3447244124062585,"name":"Janessa","tag":"Lehner"},{"id":-126042515372323,"name":"Cleveland","tag":"Willms"},{"id":-1535414615852871,"name":"Larry","tag":"Sporer"},{"id":-3884626498029511,"name":"Aisha","tag":"Hickle"},{"id":-62864875187123,"name":"Erna","tag":"Breitenberg"},{"id":-4529704642166651,"name":"Antonina","tag":"Stokes"},{"id":-8709946353240331,"name":"Lyda","tag":"Kohler"},{"id":1576672702016889,"name":"Isabelle","tag":"Witting"},{"id":1491912517659265,"name":"Magdalen","tag":"Bernier"},{"id":5612660677712093,"name":"Alessandra","tag":"Reichel"},{"id":-4326028071744395,"name":"Oswaldo","tag":"Dietrich"},{"id":2089655298148077,"name":"Caden","tag":"Waelchi"},{"id":2376153574856501,"name":"Loma","tag":"Homenick"},{"id":4135479582232353,"name":"Kaley","tag":"Turcotte"},{"id":-8973834849086155,"name":"Ottis","tag":"Bins"},{"id":-4742893635705675,"name":"Crawford","tag":"Treutel"},{"id":2863951656386065,"name":"Ali","tag":"Bailey"},{"id":-8518308455602767,"name":"Carolyn","tag":"Ratke"},{"id":2734956108648521,"name":"Eliza","tag":"Homenick"},{"id":8188560924229177,"name":"Mariana","tag":"Zboncak"},{"id":5438627973044189,"name":"Henriette","tag":"Rowe"},{"id":-1332235468976667,"name":"Giovanna","tag":"Spinka"},{"id":-8712578627922479,"name":"Damaris","tag":"Gutkowski"},{"id":4164585918473653,"name":"Sigrid","tag":"Hauck"},{"id":-3328404122896507,"name":"Tavares","tag":"Schultz"},{"id":-7526045792752251,"name":"Imelda","tag":"Rogahn"},{"id":607631701405501,"name":"Eloy","tag":"Corwin"},{"id":-2128768902316175,"name":"Ernestine","tag":"Dickinson"},{"id":-2560493300577811,"name":"Eliseo","tag":"Bernier"},{"id":76696219314037,"name":"Gus","tag":"Upton"},{"id":-5983164546117839,"name":"Clement","tag":"Fadel"},{"id":8544660048041465,"name":"Chanel","tag":"Willms"},{"id":3373011494885005,"name":"Vita","tag":"West"},{"id":1073442677445557,"name":"Juliet","tag":"McGlynn"},{"id":-7119643117445863,"name":"Trey","tag":"Mills"},{"id":-8625502121434695,"name":"Moriah","tag":"Ferry"},{"id":-6882957502964655,"name":"Monserrat","tag":"Metz"},{"id":-8398370814159091,"name":"Aidan","tag":"Bednar"},{"id":2900840607953937,"name":"Khalil","tag":"Hodkiewicz"},{"id":2644347685027561,"name":"Kellen","tag":"Balistreri"},{"id":615002744108373,"name":"Geo","tag":"Cummings"},{"id":6952105999079573,"name":"Penelope","tag":"Morar"},{"id":-7526415498579467,"name":"Karson","tag":"Rau"},{"id":8851447348052761,"name":"Eda","tag":"Brakus"},{"id":6220485738480145,"name":"Theodora","tag":"Schinner"},{"id":5099140087109749,"name":"Alessandro","tag":"White"},{"id":4505282930417813,"name":"Duncan","tag":"Stanton"},{"id":-3297202129914999,"name":"Carlee","tag":"Herzog"},{"id":4282223473452493,"name":"Jayson","tag":"Cruickshank"},{"id":-1801193353458443,"name":"Lonny","tag":"Haag"},{"id":-7513293952462587,"name":"Julio","tag":"Kovacek"},{"id":8556578150453721,"name":"Kathryne","tag":"Will"},{"id":-6645154000691575,"name":"Lisa","tag":"Wuckert"},{"id":8425159408455953,"name":"Kay","tag":"Dooley"},{"id":1501742821196077,"name":"Ruben","tag":"Altenwerth"},{"id":8123459667936233,"name":"Benny","tag":"Kertzmann"},{"id":1830224405387545,"name":"Kip","tag":"Nader"},{"id":5848753421919885,"name":"Carole","tag":"Cummerata"},{"id":5384289099462913,"name":"Jamil","tag":"Homenick"},{"id":7233610415886773,"name":"Sarina","tag":"Lehner"},{"id":-1039109859733191,"name":"Ahmed","tag":"Kemmer"},{"id":5269928734464049,"name":"Jorge","tag":"Marks"},{"id":-3214501187679819,"name":"Kendrick","tag":"Bayer"},{"id":-2607592443213567,"name":"Milan","tag":"Stanton"},{"id":-3980098798137767,"name":"Lori","tag":"Gleichner"},{"id":5223564098533665,"name":"Cortney","tag":"Dickens"},{"id":-7676876783040339,"name":"Faustino","tag":"Mohr"},{"id":-3707555554005387,"name":"Cecilia","tag":"Schneider"},{"id":8701954899491505,"name":"Zaria","tag":"Grady"},{"id":-4345378678674739,"name":"Jamil","tag":"Von"},{"id":-3060525399491583,"name":"Wellington","tag":"Ritchie"},{"id":-1232292974478587,"name":"Ariel","tag":"Konopelski"},{"id":-3518674475737475,"name":"Ivy","tag":"Larson"},{"id":4505826437718261,"name":"Francis","tag":"Kub"},{"id":5569583946064801,"name":"Kade","tag":"Fisher"},{"id":-7959488046914443,"name":"Eveline","tag":"Hoeger"},{"id":-4362689616842059,"name":"Judy","tag":"Purdy"},{"id":-3106989878620275,"name":"Sylvester","tag":"Breitenberg"},{"id":-6167595592427183,"name":"Baby","tag":"Mann"},{"id":6521242684926261,"name":"Elliot","tag":"Altenwerth"},{"id":-2647845104741835,"name":"Lorena","tag":"Torp"},{"id":-8787252923796875,"name":"Reyes","tag":"Bernhard"}]%
POSTでの作成も試してみましょう。
$ curl localhost:4010/pets -X POST -v
* Trying 127.0.0.1:4010...
* Connected to localhost (127.0.0.1) port 4010 (#0)
> POST /pets HTTP/1.1
> Host: localhost:4010
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/1.1 201 Created
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Headers: *
< Access-Control-Allow-Credentials: true
< Access-Control-Expose-Headers: *
< Date: Fri, 01 Dec 2023 04:29:31 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
< Content-Length: 0
<
* Connection #0 to host localhost left intact
パラメーターなしでPOSTで作成ってなんなんだって感じですけど、一応201で成功しています。
GETで pets/{petId}
を試してみましょう。
$ curl localhost:4010/pets/1 -H "Prefer: dynamic=true"
{"id":3331585163813937,"name":"Amie","tag":"Hagenes"}
いい感じですね!
Rustのクライアントコードを自動生成する
package.json
を編集してgenerate
コマンドを作成します。
"generate": "openapi-generator-cli generate -g rust -i petstore.yaml -o petstore",
pnpm generate
を実行すると、petstore
ディレクトリが作成され、その中にRustのクライアントコードが自動生成されます。
Rustクライアントのテストコードを書いていく
まずは <PROJECT_ROOT>/Cargo.toml
を編集します。
# 他の要素は全て消して大丈夫です
[workspaces]
members = [
"petstore"
]
cd petstore
<PROJECT_ROOT>/petstore/Cargo.toml
を編集します。
[package]
name = "petstore_api"
# .....中略
[dependencies]
tokio = { version = "1.34.0", features = ["full"] }
mkdir tests
touch tests/api.rs
tests/api.rs
を編集します。
申し訳程度のテストコードですが、こんな感じで書けます。
extern crate petstore_api;
#[cfg(test)]
mod tests {
use petstore_api::apis::configuration::Configuration;
use petstore_api::apis::pets_api::{create_pets, list_pets, show_pet_by_id};
fn test_config() -> Configuration {
let client = reqwest::ClientBuilder::new();
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::CONTENT_TYPE,
reqwest::header::HeaderValue::from_static("application/json"),
);
headers.insert(
reqwest::header::HeaderName::from_static("prefer"),
reqwest::header::HeaderValue::from_static("dynamic=true"),
);
let client = client
.default_headers(headers)
.build()
.expect("failed to build reqwest client");
Configuration {
base_path: "http://localhost:4010".to_string(),
client,
..Default::default()
}
}
#[tokio::test]
async fn test_create_pets() {
let config = test_config();
let result = create_pets(&config).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_pets() {
let config = test_config();
let result = list_pets(&config, None).await;
assert!(result.is_ok());
let pets = result.unwrap();
assert_eq!(pets.len(), 100);
let pet = &pets[0];
assert!(!pet.name.is_empty());
}
#[tokio::test]
async fn test_show_pet_by_id() {
let config = test_config();
let result = show_pet_by_id(&config, "1").await;
assert!(result.is_ok());
let pet = result.unwrap();
assert!(!pet.name.is_empty());
}
}
テストを実行してみましょう。
cargo test
Compiling petstore_api v1.0.0 (/Users/serinuntius/src/github.com/serinuntius/openapi-gen/petstore)
Finished test [unoptimized + debuginfo] target(s) in 1.65s
Running unittests src/lib.rs (target/debug/deps/petstore_api-1d787ec708584cd2)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/api.rs (target/debug/deps/api-abfc6b295cc8b796)
running 3 tests
test tests::test_create_pets ... ok
test tests::test_show_pet_by_id ... ok
test tests::test_list_pets ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
Doc-tests petstore_api
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
無事にテストが通りました!
成果物はこちら
まとめ:OpenAPIとRustで効率的なAPI開発
-
OpenAPIを使用したモックAPIの作成: OpenAPIの定義書を用いて、Prismを使ってモックサーバーを設定し、ペットショップAPIのモックバージョンを作成しました!
-
Rustのクライアントコードの自動生成: OpenAPI Generator CLIを使用して、OpenAPIの定義書からRustのクライアントコードを自動生成しました!
-
テストコードの作成と実行: RustでAPIクライアントのテストコードを作成し、モックサーバーに対する各APIメソッド(ペットの作成、一覧表示、IDによる検索)のテストを実行!
このプロセスを通じて、APIの開発とテストが効率的に行われ、RustにおけるAPIクライアントの実装が容易にできます!
no plan株式会社について
- no plan株式会社は 「テクノロジーの力でZEROから未来を創造する、精鋭クリエイター集団」 です。
- ブロックチェーン/AI技術をはじめとした、Webサイト開発、ネイティブアプリ開発、チーム育成、などWebサービス全般の開発から運用や教育、支援なども行っています。よくわからない、ふわふわしたノープラン状態でも大丈夫!ご一緒にプランを立てていきましょう!
- no plan株式会社について
- no plan株式会社 | web3実績
- no plan株式会社 | ブログ一覧
エンジニアの採用も積極的に行なっていますので、興味がある方は是非ご連絡ください!
参考文献
Discussion