abstract_notifier gemがいい感じ。
abstract_notifierはPush Notificationのようなテキストベースの通知をActionMailerと同じような感じに実装するためのgemです。色々やってくれるというより、名前の通り枠組みを提供する感じです。
実装
要するに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.deliveries
とAbstractNotifier::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のブログ