Crafting user notifications in Rails with Active Delivery
Tags: ruby, rails, notifications, showdev
Published: 2019-01-15 15:51:03

---
title: Crafting user notifications in Rails with Active Delivery
published: true
canonical_url: https://evilmartians.com/chronicles/crafting-user-notifications-in-rails-with-active-delivery
description: Introducing active_delivery and abstract_notifier Ruby gems
tags: ruby, rails, notifications, showdev
---

[Rails](https://rubyonrails.org) framework is a like a Swiss-army knife, providing a lot of useful functionality out-of-the-box (and [it's becoming](https://weblog.rubyonrails.org/2018/12/20/timeline-for-the-release-of-Rails-6-0/) even more _Swiss-er_).

It's built on top of the _sub-frameworks_, such as, to name a few, ActiveRecord, ActiveJob, ActionCable (❤️), **ActionMailer**... Ok, let's stop at this point.
What is the purpose of ActionMailer?

ActionMailer is an **abstraction to send emails** (and receive too, though now we have [ActionMailbox](https://edgeguides.rubyonrails.org/action_mailbox_basics.html)).

It abstracts the delivery mechanism and provides a _Railsy_ API to _build_ messages.

So, sending email notifications to users is not a big problem for Rails apps.

The problem is that in a modern world we have many different ways to send notifications, not only emails: push notifications, chatbots, SMS, _pigeons_.

**NOTE:** DDH [mentioned](https://youtu.be/m1jOWu7woKM?t=1096) some "action notifier" framework, "yet to be extracted" from Basecamp, which sounds like a solution; but we're not there yet.

It's pretty common to have a code like this:

```ruby
def notify_user(user)
  MyMailer.with(user: user).some_action.deliver_later if user.receive_emails?
  SmsSender.send_message(user, "Something happened") if user.receive_sms?
  NotifyService.send_notification(user, "action") if whatever_else?
end
```

And there could be dozens of such places in the codebase. Good luck with maintaining and testing this code!

How can we refactor this code? Maybe, we need an _another layer of abstraction_?)

Here comes [Active Delivery](https://github.com/palkan/active_delivery)–a new gem I wrote to solve this puzzle.

Active Delivery is a framework providing an entry point for all types of notifications: mailers, push notifications, whatever you want.

It helps you to rewrite the code above in the following way:

```ruby
def notify_user(user)
  MyDelivery.with(user: user).notify(:some_action)
end
```

And even more–you can now test it elegantly:

```ruby
# my_something_spec.rb
expect { subject }.to have_delivered_to(MyDelivery, :some_action).
  with(user: user)
```

How does it work?

In the simplest case, a _delivery_ is just a wrapper over a mailer:

```ruby
# suppose that you have a mailer class
class MyMailer < ApplicationMailer
  def some_action
    # ...
  end
end

# the corresponding delivery could look like this
class MyDelivery < ActiveDelivery::Base
  # here we can also apply "delivery rules"
  before_notify :ensure_receive_emails, on: :mailer
  
  def ensure_receive_emails
    # returning `false` halts the execution
    params[:user].receive_emails?
  end
end

# when you call
MyDelivery.with(user: user).notify(:some_action)

# it invokes under the hood (only if user receives emails)
MyMailer.with(user: user).some_action.deliver_later
```

We rely on **convention over configuration** to infer the corresponding mailer class.

OK. We've just wrapped our mailer. What's the deal? How to handle other delivery methods?

Let's take a look at the architecture of the framework:

![Active Delivery architecture](https://thepracticaldev.s3.amazonaws.com/i/4l7xds58nedq12aqmvoi.jpg)

Notice that we have an internal layer here–_lines_. Each line is a connector between the delivery and the actual notification channel (e.g., mailer).

Active Delivery provides an API to [add custom delivery _lines_](https://github.com/palkan/active_delivery#custom-lines)–that's how you can implement pretty much any type of notifications!

And to make it even easier, we've built another micro-framework–[Abstract Notifier](https://github.com/palkan/abstract_notifier).

It's a very _abstract_ framework: all it does is provides an Action Mailer-like API for describing _notifier_ classes, pure Ruby abstraction, zero knowledge of "how to send notifications."

Why Action Mailer-like interface? It's a familiar and continent API, first of all. And I like it's [parameterized classes](https://api.rubyonrails.org/classes/ActionMailer/Parameterized.html) feature (which we heavily use in Active Delivery).

To "teach" Abstract Notifier how to send notifications, you must implement a _driver_ (any callable object).

For example, we use [Twilio Notify](https://twilio.com/notify) for push notifications, and that's how our driver, `ApplicationDelivery` and `ApplicationNotifier` classes look like:

```ruby
class TwilioDriver
  attr_reader :service

  def initialize(service_id)
    client = build_twilio_api_client
    @service = client.notify.services(service_id)
  end

  def call(params)
    service.notifications.create(params)
  end
end

class ApplicationDelivery < ActiveDelivery::Base
  # NOTE: abstract_notifier automatically registers its default line,
  # you don't have to do that
  #
  # Default notifier infers notifier classes replacing "*Delivery* with
  # "*Notifier"
  register_line :notifier, ActiveDelivery::Lines::Notifier
end

class ApplicationNotifier < AbstractNotifier::Base
  self.driver = TwilioDriver.new(Rails.application.config.twilio_notify_id)
end
```

Now let's define our delivery, mailer and notifier classes:

```ruby
class PostsDelivery < ApplicationDelivery
  # here we can define callbacks, for example,
  # we want to enforce passing a target user as a param
  before_notify :ensure_user_provided  

  def ensure_user_provided
    raise ArgumentError, "User must be passed as a param" unless params[:user].is_a?(User)
  end

  # in our case we have a convenient params-reader method
  def user
    params[:user]
  end
end

class PostsMailer < ApplicationMailer
  def published(post)
    mail(
      to: user.email,
      subject: "Post #{post.title} has been published"
    )
  end
end

class PostsNotifier < ApplicationNotifier
  # Btw, we can specify default notification fields
  default action: "POSTS"

  def published(post)
    notification(
      body: "Post #{post.title} has been published",
      identity: user.twilio_notify_id
      # you can pass here any fields supported by your driver
    )
  end
end
```

And, finally, that's how we trigger the notification:

```ruby
PostsDelivery.with(user: user).notify(:published, post)
```

What if need one more notification channel? We can add another notifier line to our  `ApplicationDelivery`:

```ruby
class ApplicationDelivery < ActiveDelivery::Base
  register_line :notifier, ActiveDelivery::Lines::Notifier
  
  register_line :pigeon,
                ActiveDelivery::Lines::Notifier,
                # resolver is responsible for inferring
                # the notifier class from
                # the delivery class name
                resolver: ->(name) { name.gsub(/Delivery$/, "Pigeon").safe_constantize }
    
end

class PigeonNotifier < AbstractNotifier::Base
  self.driver = PigeonDelivery.new
end

class PostsPigeon < PigeonNotifier
  def published(post)
    notification(
      to: user.pigeon_nest_id,
      message: "coo-chi coo-chi coo"
    )
  end
end
```

That's it 🐦!

Check out [Active Delivery](https://github.com/palkan/active_delivery#custom-lines) and [Abstract Notifier](https://github.com/palkan/abstract_notifier) repos for more technical information.

---

Read more dev articles on [https://evilmartians.com/chronicles](https://evilmartians.com/chronicles)!