Rust で API を作成して Rails の便利さを改めて実感した話

# 目次

- [はじめに](#はじめに)
- [想定読者](#想定読者)
- [作成した API について](#作成した-api-について)
  - [概要](#概要)
  - [開発環境](#開発環境)
  - [フォルダー構成](#フォルダー構成)
- [本題](#本題)
  - [Rocket & diesel と Rails それぞれの実装に対する比較](#rocketdiesel-と-rails-それぞれの実装に対する比較)
        - [目的 1. API として使用できるために、Json や Status を設定できるようにする](#目的-1-api-として使用できるためにjson-や-status-を設定できるようにする)
        - [目的 2. User 作成時に、 Validation からエラーメッセージを取得し、Json で返却する](#目的-2-user-作成時に-validation-からエラーメッセージを取得しjson-で返却する)
        - [目的 3. Request Header の Token を用いて、ユーザー認証を行う](#目的-3-request-header-の-token-を用いてユーザー認証を行う)
  - [Rocket & diesel(Rust) で開発していて困ったこと](#rocket--dieselrust-で開発していて困ったこと)
- [最後に](#最後に)

# はじめに

こんにちは DeNA22 卒内定者の takaya です。
この記事は[DeNA 21 新卒 ×22 新卒内定者 Advent Calendar 2021](https://qiita.com/advent-calendar/2021/dena-21x22) の 24 日目の記事です。

自分は、最近触り始めた **Rust(Rocket & diesel)** で **API** を作成してみたので、**Rails との比較**について書いてみようと思います。
よろしければ、ぜひご覧ください。

今回作成した API は、以前自分で作成した **Rails API** をモチーフにしています。
`Rails API についての記事`はこちら [Rails チュートリアルを完全 SPA 化してみた](https://qiita.com/takaya787/items/86e0ee24b2e148a755fe)
`レポジトリー`はこちら**(Git hub)** [Rails API Repository](https://github.com/takaya787/tut_backend)
`Open API Document`はこちら**(Swagger hub)** [rails-tutorial-open-api](https://app.swaggerhub.com/apis/takaya787/rails-tutorial-api/1.0.0)

# 想定読者

- **Rails** 経験がある方
- **Rust** での **API** 作成に興味がある方
- **Rocket** や **diesel** に興味がある方

**今回の記事の目的**は、あくまで **Rails** と **Rust(Rocket & diesel)** での **API 作成の流れ**や**コードの量の違い**について実際のコードを比較しながら感じて貰うことなので、載せているコードに対しての解説は少なめです。

# 作成した API について

### 概要

自分が**Rails** で作成した API を、**Rust** で再現してみようと思って始めたので、local の`Docker-compose`の `service`に**rust**環境を追加して開発しています。
データベーズは**Rails**で使用している**postgres**を併用していて、**ORM**には、**Rust**製の**diesel**、**Web フレームワーク**もまた同様に、**Rust**製の**Rocket**を使用しています。

今回作成した`Rust API のレポジトリー`はこちら**(Git hub)** [Rails Demo API with Rust](https://github.com/takaya787/rust-app)

### 開発環境

- `Docker, Docker-compose`
- `Rust:1.56 (nightly)`
- `diesel:1.4.8 { features: "postgres,chrono" }`
- `rucket:0.5.0-rc.1 { features: "json" }`
- `postgres SQL`

### フォルダー構成

```
.
├── Cargo.lock
├── Cargo.toml
├── LICENCE
├── README.md
├── Rocket.toml
├── diesel.toml
├── migrations
│   └── 00000000000000_diesel_initial_setup
├── rust-toolchain.toml
├── src
│   ├── controllers
│   │   ├── auth.rs
│   │   ├── microposts.rs
│   │   ├── mod.rs
│   │   └── users.rs
│   ├── helpers
│   │   ├── auth.rs
│   │   ├── microposts.rs
│   │   ├── common.rs
│   │   ├── mod.rs
│   │   └── users.rs
│   ├── lib.rs
│   ├── main.rs
│   ├── models
│   │   ├── forms.rs
│   │   ├── indexes.rs
│   │   ├── mod.rs
│   │   └── tables.rs
│   └── schema.rs
└── target
```

フォルダー構成は、**Rails**をモチーフにしています。
**models**は 使用する**Model**、**controllers**は**request**を受け付けるための**controller 関数**、**helpers**は**helper 関数**をそれぞれ定義しています。
各`${controller}.rs`には、対応する`${helper}.rs`を作成し、その`${helper}.rs`と`commmon.rs`のみをインポートしています。
`lib.rs`は、 API 内で、共通して使用しているコードを定義しています。

# 本題

ではここから、今回の記事のメインである、**Rails**と**Rust(Rocket & diesel)**の比較について書こうと思います。

## Rocket,diesel と Rails それぞれの実装に対する比較

まず、実現したい目的に対しての**Rocket,diesel**で必要なコードを記載して、それから**Rails**でのコードを記載することで、比較していこうと思います。

### 目的 1. API として使用できるために、Json や Status を設定できるようにする

- **Rocket,diesel での実装**

**Rocket**は、**標準では `Json型` と `Status型` を自分で設定して返却することができない**ので、返却用の`Responder型`を自分で定義する必要がありました。
まず、`Json型`と`Status型`を保持できる`ApiResponse` **struct** を作成します。

```rust:lib.rs
use rocket::http::{ContentType, Status};
use rocket::serde::json::Value;
  // ControllerからJsonを返すAPI Responseを定義
#[derive(Debug)]
pub struct ApiResponse {
  pub json: Value,
  pub status: Status,
}
```

次に、`ApiResponse` **struct** に、`controller`の返り値となる`Responder型`を継承して、返り値にプロパティーである**status**と**json**の値が、反映されるようにしてようやく準備完了です。

```rust:lib.rs
use rocket::response::{Responder, Response};

# 上記のコードの続きに追加
# 前述したApiResponseオブジェクトにResponderトレイトを継承
impl<'r, 'o: 'r> Responder<'r, 'o> for ApiResponse {
  fn respond_to(self, _: &Request) -> response::Result<'o> {
    Response::build()
      .status(self.status)
      .header(ContentType::JSON)
      .streamed_body(Cursor::new(self.json.to_string()))
      .ok()
  }
}
```

あとは`controller`の返り値に`ApiResponse` **struct** を設定することで、**response** の`Status`や`Json`を自由に設定できるようになります。
最後に`controller`から`ApiResponse` **struct** を返却する関数を作成して完了です。

```rust:controller.rs
use crate:*;
use rocket::serde::json::json;
use rocket::http::Status;

pub fn sample() -> ApiResponse {
  ApiResponse::new(
    Status::Ok,
    json!({
      "name": "sample",
      "age": 20,
    }),
  )
}
```
- **Rails での実装**

ですが、**Rails**では上記のような設定は一切不要です。 笑
標準で上記の機能を実装してくれているので、同じことが次のコードのみで可能です。

```ruby:controller.rb
def sample
  json = { "name" => "sample", "age" => 20 }
  render json: json, status: :ok
  # jbuilderを用いる場合は、下記のコードになる
  render "users/index",
         formats: :json,
         handlers: "jbuilder",
         status: :ok
end
```

#### 目的 1 に対してのまとめ

今回に関しては、 **Rails** が初心者向けて言われる理由が顕著に出てる気がします。
やりたいことを自分で設定する必要がなく、ドキュメント通りにするだけでできるのが一番簡単ですね！
**Rocket** が初心者に優しくない気もしますが。

### 目的 2. User 作成時に、 Validation からエラーメッセージを取得し、Json で返却する

**Validation**とは、ユーザー作成や、ログインなどの際、**受け取るパラメーターが不正な値**の場合、不正な**request**をガードして、**エラーメッセージ**を取得するための仕組みです。
新しいユーザーなどのオブジェクトを作成する際に、**不適切なオブジェクトが作成されることを妨げる目的**で、設定されます。

- **Rocket,diesel での実装**

**Rocket**では、各`controller`を不正なパラメーターから守るための仕組みとして、**Request Guard**というものがあります。
ですので**Rocket**では**Request Guard**を用いて、以下の手順で**Validation**の**エラーメッセージ**を取得します。

- 1. `controller`が取得するフォームの**Model**を作成して、そこに**Validation**を設定する
- 2. `controller`関数の引数に型を設定して、**Request Guard**を有効にする
- 3. `User`追加用のモデル(`NewUser`)を作成して、データベースに追加する

手順が多いので、一つずつ取り上げていきます。

- 1. `Rocket`ではまず`controller`が取得するフォームの**Model**を作成して、そこに**Validation**を設定する

今回は`User Form`の一例を示します。
`User Form`の**Validation**は、**name**、**password**に長さの制限を設けて、また**password_confirmation**が**password**と正しいかどうかを確認しています。

```rust:forms.rs
use rocket::form::FromForm;
use serde::Deserialize;

#[derive(Debug, Deserialize, FromForm)]
pub struct UserForm<'r> {
  #[field(validate = len(7..20))]
  pub name: &'r str,
  pub email: &'r str,
  #[field(validate = len(7..20))]
  pub password: &'r str,
  #[field(validate = dbg_eq(self.password))]
  pub password_confirmation: &'r str,
}
```

- 2. `controller`関数の引数に**型**を設定して、**Request Guard**を有効にする

`Rocket`では、`controller`関数の引数に**型**を設定することで、**Request Guard**を設定することができます。今回は、**Validation Error**を取得してユーザーに表示したかったので、関数の引数に**エラー**を扱える`Contextual`型を利用しました。
`controller`内では、**Request Guard**を通らず`value`を持たない時に、**Validation Error**を扱うための`handle_user_form_validation`関数を実行しています。

```rust:controllers/user.rs
use rocket::form::{Contextual, Form};
# controllerメソッドに、UserFormを引数に設定して、型Guardを設定
#[post("/users", data = "<user_form>")]
pub fn create(user_form: Form<Contextual<'_, UserForm>>) -> ApiResponse {
  if user_form.value.as_ref().is_none() {
    return handle_user_form_validation(user_form);
  }
}
```

```rust:helpers/user.rs
pub fn handle_user_form_validation(user_form: Form<Contextual<'_, UserForm>>) -> ApiResponse {
  let err_item = user_form.context.errors().next().unwrap();
  let key: String = err_item.name.as_ref().unwrap().to_string();
  let value: String = err_item.kind.to_string();

  return ApiResponse {
    status: Status::UnprocessableEntity,
    json: json!({"errors":  {key: [value]}}),
  };
}
```

- 3. `User`追加用のモデル(`NewUser`)を作成して、データベースに追加する処理を行う

この時点で、`User Form`にはエラーがないことが保証されているので、`NewUser`を通して、新しいユーザーを作成する**ヘルパー関数**(`create_user`)をしています。
ユーザー作成は**diesel**を通して行われて、`create_user`内で実行しています。
ユーザー作成の処理の際に、**データベース**を通して**email**がユニークであるかを確認し、同じ**email**が存在する場合はエラーを返すという流れです。

```rust:controllers/user.rs
#[post("/users", data = "<user_form>")]
pub fn create(user_form: Form<Contextual<'_, UserForm>>) -> ApiResponse {
# User Form のエラーを予め処理する
# User Form の値を利用して、userを作成する
  let result = create_user(&conn, user_form.value.as_ref().unwrap());

# データベースのエラーを受け取ってresponseに変換する
  let error_response = handle_diesel_error(&result);

  if error_response.is_some() {
    return error_response.unwrap();
  }
}
```

```rust:helpers/user.rs
// Post /users
impl NewUser {
  fn create(name: &str, email: String, password_digest: String) -> NewUser {
    NewUser {
      name: Some(String::from(name)),
      email: Some(email),
      created_at: Utc::now().naive_utc(),
      updated_at: Utc::now().naive_utc(),
      password_digest: Some(password_digest),
      admin: false,
      activation_digest: None,
      activated: Some(true),
      activated_at: None,
      reset_digest: None,
      reset_sent_at: None,
    }
  }
}

// ユーザーを作成するためのhelper関数
pub fn create_user<'a>(conn: &PgConnection, userform: &UserForm) -> QueryResult<User> {
  use schema::users;
  let password_hash = hash(userform.password, DEFAULT_COST).expect("Error hashing password");
# user作成用のobject new_userを作成する
  let new_user = NewUser::create(
    userform.name,
    userform.email.to_lowercase(),
    String::from(password_digest),
  );

# dieselを通して、データベースのusers tableにNewUserを追加する
  diesel::insert_into(users::table)
    .values(&new_user)
    .get_result::<User>(conn)
}
```

```rust:models/tables.rs
#[derive(Debug, Queryable, Insertable)]
#[table_name = "users"]
pub struct NewUser {
  pub name: Option<String>,
  pub email: Option<String>,
  pub created_at: NaiveDateTime,
  pub updated_at: NaiveDateTime,
  pub password_digest: Option<String>,
  pub admin: bool,
  pub activation_digest: Option<String>,
  pub activated: Option<bool>,
  pub activated_at: Option<NaiveDateTime>,
  pub reset_digest: Option<String>,
  pub reset_sent_at: Option<NaiveDateTime>,
}
```

- **Rails での実装**

ではここで、**Rails**で**Validation Error**を表示するやり方と比較してみます。
**Rails**では以下の手順で同様の処理を実装できます。

- 1. **Model**に**Validation**を設定する
- 2. フォームから**params**を安全に受け取るために、`strong parameters`を設定する
- 3. **Model**のインスタンスを作成して、データベースに追加する処理を行う

手順として多く見えますが、実際のコード量は **Rust** の時とは大きく違います。

- 1. **Model**に**Validation**を設定する

```ruby:models/user.rb
class User < ApplicationRecord

  #gem bcryptを追加したら利用でき、Userにpassword_digestを作成してくれる
  has_secure_password
  # User Modelに設定するValidationを定義
  validates(:name, presence: true, length: { maximum: 50 })
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates(:email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false })
  validates(:password, presence: true, length: { minimum: 6 }, allow_nil: true)
end
```

- 2. フォームから**params**を安全に受け取るために、`strong parameters`を設定する

安全に Params を受け取るための`strong parameters`を設定する必要があります。
このパラメーターはユーザー作成時以外使用しないので、`private`メソッドで定義します。

```ruby
private
# 安全にParamsを受け取るためのstrong parametersを設定する
def user_params
  params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
```

- 3. **Model**のインスタンスを作成して、データベースに追加する処理を行う

最後にユーザー作成を行うのですが、その際に**Model Validation** によるエラーメッセージを**rails では自動的に得ること**ができます。
**rocket**では**エラーメッセージ**を取得するために、生じた`Error`に対応するメッセージを自分で作成する必要がありました。
ですが、**rails**では**エラーメッセージ**を`errors`として作成してくれているので、後は`json`で返却するだけで実装できます。

```ruby:controllers/users.rb
# POST /users
def create
  @user = User.new(user_params)
  if @user.save
    @user.send_activation_email
    payload = { user_id: @user.id }
    token = encode_token(payload)
    render json: { user: @user, token: token, gravator_url: gravator_for(@user) }, status: :created, location: api_user_url(@user)
  else
    # saveに失敗した際に、@userにerrorsが追加される
    render json: { errors: @user.errors }, status: :bad_request
  end
end

# 2のstrong parameterを定義
def user_params
  params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
```

####目的 2 に対してのまとめ
**Rust(Rocket & diesel)** では `Error型`を受け取って、自分で**エラーメッセージ**に変換する必要があったので、**rails**の**Validation**から自動的に**エラーメッセージ**を作成してくれるところは、かなり便利だなと思いました。
何より、すぐに`errors`変数として使用できる点が良いですね！

### 目的 3. Request Header の Token を用いて、ユーザー認証を行う

最後は User 認証についてです。
**Rails**では User 認証に `JsonWebToken`を用いているので、**Rocket**で`JsonWebToken`を用いた認証についても比較してみます。

- **Rocket,diesel での実装**

**Rocket,diesel** での User 認証の処理は以下の流れで行います。

- 1. `LoginUser`を取得する**Request Guard**を作成する
- 2. `controller`に**Request Guard**を設定する

では一つずつ取り上げて行きます。

- 1. `LoginUser`を取得するための**Request Guard**を作成する

**Rocket**では、`Request`に対して特定の処理を追加して**Request Guard**を作成するための**trait**、`FromRequest`があります。
ですので、`FromRequest`を継承して`LoginUser`という**Request Guard**を作成します。

```rust:lib.rs
// User認証を行うRequestGuard
use jsonwebtoken::{decode, DecodingKey, Validation};
use models::tables::User as LoginUser;

#[rocket::async_trait]
impl<'r> FromRequest<'r> for LoginUser {
    type Error = UserAuthError;

    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        // Request Headerから JWTを取得
        let token: &str = match request.headers().get_one("Authorization") {
            Some(token) => token.split_whitespace().collect::<Vec<&str>>()[1],
            None => return Outcome::Failure((Status::Unauthorized, UserAuthError::NotFoundToken)),
        };

        #[derive(Debug, Deserialize)]
        struct DecodedToken {
            user_id: i64,
        }

        // tokenからuser_idにdecode
        let decoded_token = decode::<DecodedToken>(
            &token,
            &DecodingKey::from_secret("s3cr3t".as_ref()),
            &Validation {
                validate_exp: false,
                ..Default::default()
            },
        );

        if let Err(_) = decoded_token {
            return Outcome::Failure((Status::Unauthorized, UserAuthError::InvalidToken));
        }

        let conn = establish_connection();

        // decode_tokenを用いて、Login Userを取得する
        let current_user =
            helpers::users::get_user_by_id(&conn, decoded_token.unwrap().claims.user_id);

        match current_user {
            Ok(user) => Outcome::Success(user),
            Err(_) => Outcome::Failure((Status::Unauthorized, UserAuthError::NotFoundUser)),
        }
    }
}
```

```rust:helpers/users.rs
pub fn get_user_by_id(conn: &PgConnection, user_id: i64) -> QueryResult<User> {
  use schema::users::dsl::*;

  let result = users
    .filter(id.eq(user_id))
    .filter(activated.eq(true))
    .first::<User>(conn);

  result
}
```

- 2. `controller`に**Request Guard**を設定します

`controller`の引数に`Result<LoginUser,UserAuthError>`を設定することで、**Request Guard**を有効化することができます。
最後に`key`のエラーハンドリングを行うことで、`LoginUser`の値を扱う事ができるようになります。

```rust:controllers/auth.rs
use helpers::common::handle_auth_error;

pub fn auto_login(key: Result<LoginUser, UserAuthError>) -> ApiResponse {
  // keyからuserを取得する
  let login_user = match key {
    Ok(user) => user,
    Err(err) => return handle_auth_error(err),
  };

  // login_userを用いた処理を行う
}
```

```rust:helpers/common.rs
// Result<LoginUser, UserAuthError>専用のerror Handler
// Error message用のApiResponseを作成する
pub fn handle_auth_error(error: UserAuthError) -> ApiResponse {
  println!("{:?}", error);
  match error {
    UserAuthError::NotFoundToken => ApiResponse::new(
      Status::Unauthorized,
      json!({
        "error": "Token is not found",
        "message": "Please login"
      }),
    ),
    UserAuthError::NotFoundUser => ApiResponse::new(
      Status::Unauthorized,
      json!({
        "error": "User is not found",
        "message": "Your account is not found"
      }),
    ),
    UserAuthError::InvalidToken => ApiResponse::new(
      Status::Unauthorized,
      json!({
        "error": "Token is invalid",
        "message": "Your token is invalid. \n Please login again."
      }),
    ),
  }
}
```

- **Rails での実装**

**Rails** での User 認証の処理は以下の流れで行います。

- 1. `LoginUser`を取得するための関数(`authorized`)を定義する
- 2. `controller`の`before_action`で`authorized`を実行する

では具体的な実装について挙げます。

- 1. `LoginUser`を取得するための関数(`authorized`)を定義する

まずは、 **request header** から **token** を取得して、 **decoded_token** を作成します。
その後に、**decoded_token** の **user_id** から`User`が見つかれば `Login User` として `global variable` に設定、なければ**エラー**を表示させる仕組みです。

```ruby:controllers/application_controller.rb
# request header から token を取得
  def auth_header
    #{Authorization: 'Bearer <token>'}
    request.headers['Authorization']
  end

# 取得した token から decode_token を作成する
  def decoded_token
    if auth_header
      token = auth_header.split(' ')[1]
      #Header: {Authorization: 'Bearer <token>'}
      begin
        JWT.decode(token, '${key}', true, algorothm: 'HS256')
      rescue JWT::DecodeError
        nil
      end
    end
  end

# decode_token の user_id を用いて、User を取得する
  def logged_in_user
    if decoded_token
      user_id = decoded_token[0]['user_id']
      @login_user = User.find(user_id)
    end
  end

#login_userがなければ、errorを出す
  def authorized
    render json: { message: 'Please log in'}, status: :unauthorized unless !!logged_in_user
  end
```

- 2. `controller`の`before_action`で`authorized`を実行する

**rails**の`controller`には、`before_action`というメソッドがあります。
これはある関数を実行する前に指定した関数を実行できるというもので、**login**が必要な関数の`before_action`に`authorized`を設定すれば `@login_user` が変数として使用できます。

```ruby:controllers/auth.rb
before_action :authorized

# get '/auto_login',
def auto_login
    @gravator_url = gravator_for(@login_user)
    @current_microposts = @login_user.microposts.with_attached_image
    render "users/auto_login", formats: :json, handlers: "jbuilder"
end
```

####目的 3 に対してのまとめ
エラーハンドリングを自分で実装する必要があるので、 **Rocket** の方がコード量は多かったですけど、他と比べると差はまだ少なかったかなと個人的に感じました。
**Rails** の **before_action** の使い勝手の良さはもちろんですが、 **Rocket** の **Request Guard** が **Validation** の時よりも相性が良く、書きやすかったです。
**ruby** でも **rust** でも、**JsonWebToken** を簡単に扱えるパッケージを作成してくれている点が非常に大きかったですね。

## Rocket & diesel(Rust) で開発していて困ったこと

- エラーを検索しても、解決策がなかなか見つからない

**Rocket** や **diesel** の利用者が少ないので、エラーメッセージを検索しても**crate ドキュメント**しか出てきません。解決策を見つけるには、**ドキュメント**や**Github**の**discussion**を読みこむしかないので初心者泣かせでした。

- **Rust** の**コンパイル**が最初のうちは全く通らない

**Rust** は **C言語** と同じくらい型が厳しいので、最初のうちは全くコンパイルが通りませんでした。言語の自体のハードルの高さを実感しましたね。
**Typescript** みたいに一旦 `any` で握りつぶすみたいなことも難しいので、最初のうちは開発しててイライラしました。 笑

# 最後に

いつか競プロに挑戦してみたいなと思っていて、そのために勉強を始めた言語が **Rust** でした。なので今回の **API** 作成は、より **Rust** を理解できるきっかけになったので個人的に大満足です。
ただ僕自身は **Rust** を言語として凄い気に入ってるんですが、**Rust** が **API** 作成において**Rails** や **Typescript & Express** よりも生産性が高くなることは難しい気がしますね。
フレームワークとしての力の差を大きく感じましたし、静的型付けで開発したいなら **Typescript** 、さらにパフォーマンスも追求するなら **Go** を選ぶことがほとんどだと思います。
どの言語も適材適所なんだなと改めて実感できました。
今まで長々と書いた記事を最後まで読んで頂きありがとうございます。