abstract_notifier gemがいい感じ。

abstract_notifierはPush Notificationのようなテキストベースの通知をActionMailerと同じような感じに実装するためのgemです。色々やってくれるというより、名前の通り枠組みを提供する感じです。

実装

Image from Gyazo

要するにabstract_notifierに従って実装しておくと、

DiscordNotifier.user_signup(user).notify_now

DiscordNotifier.user_signup(user).notify_later

こんな感じにActionMailerのように同期版、非同期版を選んで実行できたり、

DiscordNotifier.with(user).user_signup.notify_later

こんな感じにActionMailerのようにParametarizedな呼び方ができます。

弊社のフィヨルドブートキャンプアプリではサイト内通知とDiscord通知をこれを使うものに置き換えました。

class DiscordNotifier < AbstractNotifier::Base
  self.driver = DiscordDriver.new
  self.async_adapter = DiscordAsyncAdapter.new
end

実装は、AbstractNotifier::Baseを継承したクラスで同期処理用のdriverと非同期用のasync_adapterを実装してセットしておくとOKです。

普通のRubyクラスでOKですが、それぞれcallメソッドとenqueueメソッドを実装している必要があります。両方ともAbstractNotifier::Baseが持ってるnotificationを経由して呼び出されます。

普通は通知でやりたいことは共通だと思うので、同期用のdriverは普通に書いて、非同期用のasync_adapterはActiveJob経由でdriverを呼び出すように書いておけば実装が共通にできていいと思います。

driverはこんな感じ。

class DiscordDriver
  def call(params)
    Discord::Notifier.message(
      params[:body],
      username: params[:name],
      url: params[:webhook_url]
    )
  end
end

async_adapterはこんな感じ。 Parameterized版はコンストラクタが呼ばれるのでenqueueでもそれが使われるように書いておく必要があります。

class DiscordAsyncAdapter
  def initialize(params = {})
    @params = params
  end

  def enqueue(_, params = {})
    params.merge!(@params)
    DiscordJob.perform_later(params)
  end
end

jobはこんな感じにしておくだけで非同期にできて便利ですね。

class DiscordJob < ApplicationJob
  queue_as :default

  def perform(params)
    DiscordDriver.new.call(params)
  end
end

テスト

テストは同期版、非同期版、Parameterized版をそれぞれテストしておくと安心です。gemにはrspec向けのhelperしかないですが、AbstractNotifier::Testing::Driver.deliveriesAbstractNotifier::Testing::Driver.enqueued_deliveriesにそれぞれdeliverしたものが入るようになっていたのでminitestではこれらを直接見る様に書くといいんじゃないかと思います。

長いのでassert_deliveryとかあった方が便利そうなのでminitest_helperを後でPRしようかなと思います。

2022年06年04日:マージされました。

abstract_notifierをminitestでテストする - komagataのブログ

require 'test_helper'

class DiscordNotifierTest < ActiveSupport::TestCase
  setup do
    @params = {
      body: 'new user signup!',
      sender: users(:bob),
      name: 'bob',
      webhook_url: 'https://discord.com/api/webhooks/0123456789/xxxxxxxx'
    }
  end

  test '.user_signup' do
    notification = DiscordNotifier.user_signup(@params)

    assert_difference -> { AbstractNotifier::Testing::Driver.deliveries.count }, 1 do
      notification.notify_now
    end

    assert_difference -> { AbstractNotifier::Testing::Driver.enqueued_deliveries.count }, 1 do
      notification.notify_later
    end

    # Parameterized
    notification = DiscordNotifier.with(@params).user_signup

    assert_difference -> { AbstractNotifier::Testing::Driver.deliveries.count }, 1 do
      notification.notify_now
    end

    assert_difference -> { AbstractNotifier::Testing::Driver.enqueued_deliveries.count }, 1 do
      notification.notify_later
    end
  end
end

ちなみにこのgemに関する情報は作者本人のブログエントリーとREADMEぐらいしか見当たらなかったのでコード読んだ方が早いです。

その後

「別に同期・非同期切り替える必要ないし、似たような実装で今動いてるから置き換えるまではいかないかな〜」

と思うかもですね。

しかし僕はこれで通知という視点で抽象化するというアイデアを知るまではDiscord通知とメール通知を同じものだと思ってなかったので共通化してスッキリしました。他のメンバーに通知を実装してもらう時もオリジナル実装よりもこういうgemになっている方がルールが明確でよさそうに思います。

そして真の目的はこれに対応させておくと後でエントリーを書こうと思っているactive_deliveryでシュッと使えるようになる点です。

つづく

active_deliveryで通知をまとめる - komagataのブログ

Comments


Option