日本の山岳一覧・百名山 API を開発しましたCLICK !

Rust 実装のモック用 REST API サーバーを起動できる CLI ツール “mocks” を開発しているというお話

mocks - Get a mock REST APIs with zero coding within seconds.
  • URLをコピーしました!

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/mocks

Cargo

Rust プログラマーで Cargo をお使いの方は cargo install でインストールすることもできます。

cargo コマンドでインストールします。

cargo install mocks

バージョン確認とヘルプコマンド

インストールしたら、以下のコマンドでバージョンを確認してみましょう。

mocks --version

--help で使い方を出力できます。

mocks --help
Get 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 version

mocks で 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/profile

Postman で http://localhost:3000/posts にリクエストを送信してみました!

Postman で mocks にリクエストを送信する

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

mocks デモ

また、実行時のオプション引数もあるので、合わせて紹介します。

-H, –host

ホストを指定することができます。(127.0.0.1localhost と同じですが…)

デフォルトは localhost です。

mocks -H 127.0.0.1 storage.json

-h--help なので注意してください。

-p, –port

ポート番号を指定することができます。

デフォルトは 3000 です。

mocks -p 8080 storage.json

–no-overwrite

これはファイルのデータを上書きしないモードです。

POST や PUT 等でデータに変更が生じた際に、実行時に指定したファイルに変更差分を反映させないように指定することができます!

ただし、メモリ上に変更差分は反映されるため、サーバー起動中は更新したデータを取得することができます。

mocks --no-overwrite storage.json

試しに実行する際には --no-overwrite を実行するのがおすすめです。

mocks の実装について

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_jsonValue という 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 の記事ですが…🤫


著:Jim Blandy, 著:Jason Orendorff, 著:Leonora F. S. Tindall, 翻訳:中田 秀基
¥5,280 (2024/12/05 05:59時点 | Amazon調べ)
著:豊田 優貴, 著:松本 健太郎, 著:吉川 哲史
¥4,400 (2024/12/05 06:00時点 | Amazon調べ)
mocks - Get a mock REST APIs with zero coding within seconds.

この記事が気に入ったら
フォローしてね!

  • URLをコピーしました!

コメント

コメントする

目次