Rust 実装のモック用 REST API サーバーを起動できる CLI ツール mocks を開発しているというお話です。
ソースコードは Github で公開しているので、気になる方は見ていってくれると嬉しいです!
スターをくれると… さらに嬉しいです! 開発の励みになります🌟
mocks – Get a mock REST APIs with zero coding within seconds.
What is mocks?
まずは mcoks について説明します!
次のような JSON 形式のファイルを指定して、モック用の REST API サーバーを起動することができます。
簡単に言ってしまうと、npm でいうところの json-server のようなツールです。
{
  "posts": [
    {
      "id": "01J7BAKH37HPG116ZRRFKHBDGB",
      "title": "first post",
      "views": 100
    },
    {
      "id": "01J7BAKH37GE8B688PT4RC7TP4",
      "title": "second post",
      "views": 10
    }
  ],
  "comments": [
    {
      "id": 1,
      "text": "a comment",
      "post_id": "01J7BAKH37HPG116ZRRFKHBDGB"
    },
    {
      "id": 2,
      "text": "another comment",
      "post_id": "01J7BAKH37HPG116ZRRFKHBDGB"
    }
  ],
  "profile": {
    "id": "01J7BAQE1GMD78FN3J0FJCNS8T",
    "name": "mocks"
  },
  "friends": []
}エンドポイントは JSON のオブジェクトを読み取って構成されます。
配列の場合は GET, POST, PUT, PATCH, DELETE が実装されます。
GET     /posts
GET     /posts/:id
POST    /posts
PUT     /posts/:id
PATCH   /posts/:id
DELETE  /posts/:id
# Same for comments and friendsオブジェクト(構造体)の場合、GET, PUT, PATCH が実装されます。
単数系なので、REST API の原則から少し外れる気もしますが、たまに見るので実装してみました!
GET     /profile
PUT     /profile
PATCH   /profileなお、ヘルスチェック用のエンドポイントは常に実装される仕様となっています。
GET     /_hc
# Health check endpoint returns a 204 response.インストール・アンインストール
インストールの方法は 2 種類あります。
Homebrew
Mac ユーザー向けのインストールコマンドです。
macOS 向けのオープンソースのパッケージ管理システムである Homebrew でインストールすることができます!
brew コマンドでインストールします。
brew install mocks-rs/tap/mocksCargo
Rust プログラマーで Cargo をお使いの方は cargo install でインストールすることもできます。
cargo コマンドでインストールします。
cargo install mocksバージョン確認とヘルプコマンド
インストールしたら、以下のコマンドでバージョンを確認してみましょう。
mocks --version--help で使い方を出力できます。
mocks --helpGet a mock REST APIs with zero coding within seconds.
Usage: mocks [OPTIONS] <FILE>
Arguments:
  <FILE>  Path of json file for data storage
Options:
  -H, --host <HOST>   Host [default: localhost]
  -p, --port <PORT>   Port [default: 3000]
      --no-overwrite  No overwrite save to json file
  -h, --help          Print help
  -V, --version       Print versionmocks で RESR API サーバーを起動する
以下のコマンドでmocks の RESR API サーバーを起動させることができます!
storage.json は任意のファイル名です。
mocks storage.json`mocks` started
Press CTRL-C to stop
Index:
http://localhost:3000
Storage files:
storage.json
Overwrite:
YES
Endpoints:
http://localhost:3000/_hc
http://localhost:3000/comments
http://localhost:3000/friends
http://localhost:3000/posts
http://localhost:3000/profilePostman で http://localhost:3000/posts にリクエストを送信してみました!

cURL でも API を叩いて動作を確認できます。

また、実行時のオプション引数もあるので、合わせて紹介します。
-H, –host
ホストを指定することができます。(127.0.0.1 は localhost と同じですが…)
デフォルトは localhost です。
mocks -H 127.0.0.1 storage.json-p, –port
ポート番号を指定することができます。
デフォルトは 3000 です。
mocks -p 8080 storage.json–no-overwrite
これはファイルのデータを上書きしないモードです。
POST や PUT 等でデータに変更が生じた際に、実行時に指定したファイルに変更差分を反映させないように指定することができます!
ただし、メモリ上に変更差分は反映されるため、サーバー起動中は更新したデータを取得することができます。
mocks --no-overwrite storage.jsonmocks の実装について
mocks の実装についても触れておきます。
clap と axum で実装
CLI ツールとしての機能は clap で実装しています。
Rust で CLI ツールを作成する場合、この clap がデファクトスタンダードと言えるでしょう。
CLI の引数パーサーの定義:mocks/src/main.rs
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
    /// Path of json file for data storage
    file: String,
    /// Host
    #[arg(short = 'H', long, default_value = "localhost")]
    host: String,
    /// Port
    #[arg(short, long, default_value_t = 3000)]
    port: u16,
    /// No overwrite save to json file
    #[arg(long, default_value_t = false)]
    no_overwrite: bool,
}また、REST API の実装は axum を使っています。
Rust における Web フレームワークはいくつかありますが、自分は axum をよく使っています。
API サーバー起動処理の実装:mocks/src/server.rs
impl Server {
    /// Starts the mock server
    ///
    /// # Arguments
    /// * `socket_addr` - The socket address to bind the server to
    /// * `url` - The base URL of the server
    /// * `storage` - The storage instance to use
    ///
    /// # Returns
    /// * `Result<(), MocksError>` - Ok if the server starts successfully, Err otherwise
    pub async fn startup(
        socket_addr: SocketAddr,
        url: &str,
        storage: Storage,
    ) -> Result<(), MocksError> {
        let listener = TcpListener::bind(socket_addr)
            .await
            .map_err(|e| MocksError::Exception(e.to_string()))?;
        println!("Endpoints:");
        print_endpoints(url, &storage.data);
        let state = AppState::new(storage);
        let router = create_router(state);
        axum::serve(listener, router)
            .await
            .map_err(|e| MocksError::Exception(e.to_string()))
    }
}ルーター構築処理の実装:mocks/src/server.rs
実行時に指定したファイルによってエンドポイントを構成するため、/:resource や /:resource/:id となるように実装しています。
fn create_router(state: SharedState) -> Router {
    let hc_router = Router::new().route("/", get(hc));
    let storage_router = Router::new()
        .route("/", get(get_all).post(post).put(put_one).patch(patch_one))
        .route("/:id", get(get_one).put(put).patch(patch).delete(delete));
    Router::new()
        .nest("/_hc", hc_router)
        .nest("/:resource", storage_router)
        .with_state(state)
}serde_json で不定型 JSON を扱う
実行時に指定したファイルによってエンドポイントを構成する仕様のため、扱うべき JSON が不定型という特徴があります。
型が決まっていないため、Serde で簡単に Serializing と Deserializing を実装できませんでした。
不定型の JSON は serde_json の Value という Enum でよしなに扱うことができます。
Value が JSON オブジェクトそのものを示す型となっています。
PATCH 時の実装:mocks/src/storage/operation/update.rs
use crate::error::MocksError;
use crate::storage::{Input, StorageData};
use serde_json::Value;
pub fn update(
    data: &mut StorageData,
    resource_key: &str,
    search_key: &str,
    input: &Input,
) -> Result<Value, MocksError> {
    let values = data
        .get_mut(resource_key)
        .and_then(Value::as_array_mut)
        .ok_or(MocksError::ResourceNotFound)?;
    let updated = values.iter_mut().find_map(|value| {
        let obj = value.as_object_mut()?;
        let id = obj.get("id")?;
        let matches = match id {
            Value::Number(key) => key.to_string() == search_key,
            Value::String(key) => key == search_key,
            _ => false,
        };
        if matches {
            if let Value::Object(input_map) = input {
                obj.extend(input_map.iter().map(|(k, v)| (k.clone(), v.clone())));
            }
            Some(value.clone())
        } else {
            None
        }
    });
    match updated {
        Some(value) => Ok(value),
        None => Err(MocksError::ObjectNotFound),
    }
}不定形であることで少し処理が複雑になってしまい、工夫が必要な箇所も多かったです。
そのおかげで実装していて、非常に勉強になりました🥳🎉
気になる方は Github でソースコードをご確認ください!
PUT 時の処理である replace.rs や DELETE 時の remove.rs も実装していて楽しかったです。
runn で E2E テスト
runn というシナリオベースのテストツールで E2E テストをできるようにしました。
YAML でテストシナリオを定義することができ、簡単に REST API をテストできます。
POST /posts のテストコード:mocks/runn-e2e/runbooks/test_post.yml
desc: "post endpoint tests"
runners:
  req:
    endpoint: http://localhost:3000
vars:
  postId: "01J85A9VQ8ZDGXDC7A00YRWKBE"
if: included
steps:
  postPost:
    desc: "Create new post"
    req:
      /posts:
        post:
          body:
            application/json:
              id: "{{ vars.postId }}"
              title: "new post"
              views: 0
    test: |
      // Status code must be 201.
      current.res.status == 201
      && current.res.body.id == vars.postId
      && current.res.body.title == "new post"
      && current.res.body.views == 0非常によく考えられたツールなので、合わせてチェックしてください!
まとめ
mocks を実装する中で、serde_json の理解が深まり、Rust の知見も得ることができました!
また、ただ実装するだけでなく、公開・配布することにも挑戦することもできました。
自分自身が Mac ユーザーなので、Mac ユーザー向けのインストール方法を優先しましたが Windows 環境でもインストールできるようにしてみたいなと考えています!
参考
自分が書いた Zenn の記事ですが…🤫
- Rust で実装したモック用 REST API サーバーを実行できる CLI を crates.io と Homebrew で公開する
- Rust | cargo build –release のバイナリサイズを削減する!
 ポチップ
					ポチップ
				 ポチップ
					ポチップ
				

 
					 
					 
	
コメント