rastam on rails

東京在住のマレーシア人 Rubyist

Ruby Weekly #359: 日本語サマリー

職場の Slack の #ruby 窓で Ruby Weekly メルマガが毎週配信されます。その中から面白そうなものをピックアップして、日本語で簡単なサマリーを書くようにしています。そのサマリーをここでまとまさせていただきます。くだけた日本語で失礼いたします。

rubyweekly.com

Highlights

Realtime with React and Rails

react_on_rails + ActionCable でリアルタイム地図上現在地配信アプリを作った話。Redux を MobX にすり替えた。Redux よりオブジェクト指向感あり。

owner の位置情報を 2 秒ごとに channel に publish。viewer が位置情報を配列に push、react-map-gl で地図上描画。

Deploying Rails Apps on Kubernetes with No Downtime

k8sRails の zero-downtime deploy を実現した話。

How DNSimple's Latest API Architecture is Built with Hanami

DNSimple の APIv2 を Hanami で作り直した話 by Hanami 作成者兼 DNSimple 社員。

メインアプリは Rails だが、API はビュー要らないから、別のフレームワークで作ることにした。

最初は Sinatra でプロトタイプを作ってたが、Hanami でも同じものを 1 時間で作ってみた。パフォーマンスはそんなに変わらなかったから Sinatra 捨てて Hanami で引き続き開発。

使ってる Hanami コンポーネントは 2 つだけ:router と action。Rails の 1 action = 1 controller メソッドのに比べて、Hanami は 1 action = 1 Hanami::Action クラスで、before_action 系フィルターはモジュール mixin。各 action が Command オブジェクト(Service オブジェクト)を呼ぶ。

結論:APIv2 は 2 年で開発完了。Hanami のおかげで保守しやすくて、パフォーマンスが良い。

News

The 'bootsnap' Gem is Now Part of the Rails Default Gemfile

新規 Rails アプリはデフォで bootsnap が Gemfile に入ってて、ブート時間 50% 短縮。

Major Coding Bootcamp Ditches Rails, Claims Lower Employer Demand

米国のメジャーなブートキャンプ Coding DojoRubyRails を教えるのをやめた。雇用先からの需要低下で。代わりに Java・Spring を教えることになった。HackerNews でもざわざわしてる。

Tutorial

Implementing Clojure's 'interpose' in Ruby

ClojureinterposeRuby で実装した話。

[1, 2, 3].interpose('x')
# [1, 'x', 2, 'x', 3]

Enumerable をモンキーパッチすることで実装した。

Quick Tip: Add Output to Your Long Running Rake Tasks

時間がかかる Rake タスクは出力しましょう、と筆者が思うようになったきっかけ。

最近こういう Rake タスク作ることになった:

  1. S3 上の全ファイル一覧取得
  2. 最終更新日時でソート
  3. ソート順に各ファイルに対してバックグラウンドジョブをキューに積む

10 分はかかるが、パフォーマンス的には問題はなかった。実行する人にとっては辛い。突然死したかどうかすら知りようがない。

解決策:

  1. 時間がかかるサブタスクは incremental feedback (プログレスバーとか)
  2. 完了時にその旨が伝わる最終出力

The Differences Between nil?, empty?, blank?, and present?

nil? empty? blank? present? presenceRuby or Rails のどっち?どんなオブジェクトに生やしてる?何が違う?などの解説。

Scope the Monkey: A Refinements Refresher

モンキーパッチするなら Refinements でスコープ制限をかけましょう。

モンキーパッチはグローバル。例えば String をモンキーパッチしたとする。外部ライブラリーと衝突するかも。既存メソッドを上書きしちゃうかも。

using マクロで特定クラス・モジュールのみにて refine ブロックのモンキーパッチを有効にする。

Gotcha

  • IRB では直接 using 呼べない
  • 例の #add1_tousing 前に 定義時点#one を呼んでいるから、Refinement がかからない

Build a Minimal Docker Container for Ruby Apps

Ruby アプリ用ドちっちゃい Docker イメージを作った話。

公式イメージだとデカい(1.6GB!)

こう作った:

  1. 公式 Alpine Linux イメージ(5MB)
  2. apk で ruby、bundler、ruby-io-console 追加
  3. apk キャッシュ削除
  4. ruby-dev ビルドパッケージ追加

The Dependency Inversion Principle

DDD の Repository で解説した依存性逆転。

class UserRepository
  def find(id:)
    User.find(id)
  end
end

User が ActiveRecord モデルだったとしたら、これが ActiveRecord に密結合しちゃう。ActiveRecord 以外の何か(例えば in-memory オブジェクトとか mongodb とか)にすり替えられない。

注入することで柔軟になる。

class UserRepository
  def initialize(data_source:)
    @data_source = data_source
  end

  def find(id:)
    User.new(@data_source.find(id))
  end
end

JSON Web Token Authentication in Rails APIs

JWT を実装した話。

通常 Rails アプリの認証はセッションのクッキーを使ってるが、API アプリでは、クッキーが使えない。そこで JWT 登場。

  1. jwt gem 入れた
  2. users.password_digest カラム追加
  3. User.has_secure_password 設定
  4. JWT エンコード・デコードは
    • 冗長なので、JwtService サービスオブジェクトでラップ
    • secret_key_base 再利用
  5. 認証が通った場合は JWT トークン発行。payload は user_id と exp(有効期限)
  6. JWT チェックは
    • ヘッダーからトークン抽出
    • user_id 存在チェック
    • 期限切れた場合は JwtService がヌルを返す
  7. 5 と 6 はそれぞれコマンドオブジェクトで実装し
  8. Concern で before_filter 定義して、7 のコマンドを呼ぶ
  9. 各コントローラで 8 の Concern を include

結論:devise に頼らずに一から作ったことで内部実装覚えた

Enforcing the Specific Location of a Method Call

Foo#bar メソッドがあるとしたら、Work#execute 以外から呼べないようにせよ。という Twitter で流れてきた問題を解決しみた話。

最初は caller 配列をチェックしてみた。動くっちゃ動くが、ファイル名と行番号をハードコードするハメになる。

クラス名とメソッド名を指定するのが理想。Work.instance_method(:execute).source_location でメソッド開始行番号とファイル名が取れる。メソッド最終行番号は、Work の全メソッドの開始行番号をソートすることで地道に計算。

Everything You Need to Know About Ruby Constants

定数なのに可変!定数のモジュールを include、extend するとどうなる?などの Ruby 定数豆知識。

Working with UUIDs in Rails

Rails + Postgres で UUID を使う方法。

  1. migration で Postgres 拡張有効化
  2. UUID カラム追加記法は 2 通り:
    • id: false (インデックス、UUID 生成は自分で実装しなきゃ)
    • id: :uuid (全部オマカセ)

UUID 以外設定しようとしたら

Person.new(id: '123')
=> #<Person id: nil>

リアル UUID の正規表現に一致しないから ActiveRecord がボツにする。