{"id":13879763,"url":"https://github.com/palkan/active_delivery","last_synced_at":"2025-05-15T09:06:06.341Z","repository":{"id":45132644,"uuid":"162626877","full_name":"palkan/active_delivery","owner":"palkan","description":"Ruby framework for keeping all types of notifications (mailers, push notifications, whatever) in one place","archived":false,"fork":false,"pushed_at":"2024-02-07T00:53:03.000Z","size":208,"stargazers_count":620,"open_issues_count":1,"forks_count":16,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-04-14T15:00:40.604Z","etag":null,"topics":["hacktoberfest","mailers","notifications","rails","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/palkan.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2018-12-20T20:12:54.000Z","updated_at":"2025-04-03T18:40:52.000Z","dependencies_parsed_at":"2024-02-13T17:15:11.715Z","dependency_job_id":null,"html_url":"https://github.com/palkan/active_delivery","commit_stats":{"total_commits":49,"total_committers":12,"mean_commits":4.083333333333333,"dds":"0.34693877551020413","last_synced_commit":"7788c857f7ecde5833d5cc41bc0d135ec9c630a2"},"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Factive_delivery","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Factive_delivery/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Factive_delivery/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Factive_delivery/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/palkan","download_url":"https://codeload.github.com/palkan/active_delivery/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254310513,"owners_count":22049468,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hacktoberfest","mailers","notifications","rails","ruby"],"created_at":"2024-08-06T08:02:32.048Z","updated_at":"2025-05-15T09:06:06.321Z","avatar_url":"https://github.com/palkan.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"[![Gem Version](https://badge.fury.io/rb/active_delivery.svg)](https://badge.fury.io/rb/active_delivery)\n![Build](https://github.com/palkan/active_delivery/workflows/Build/badge.svg)\n![JRuby Build](https://github.com/palkan/active_delivery/workflows/JRuby%20Build/badge.svg)\n\n# Active Delivery\n\nActive Delivery is a framework providing an entry point (single _interface_ or _abstraction_) for all types of notifications: mailers, push notifications, whatever you want.\n\nSince v1.0, Active Delivery is bundled with [Abstract Notifier](https://github.com/palkan/abstract_notifier). See the docs on how to create custom notifiers [below](#abstract-notifier).\n\n📖 Read the introduction post: [\"Crafting user notifications in Rails with Active Delivery\"](https://evilmartians.com/chronicles/crafting-user-notifications-in-rails-with-active-delivery)\n\n📖 Read more about designing notifications layer in Ruby on Rails applications in the [Layered design for Ruby on Rails applications](https://www.packtpub.com/product/layered-design-for-ruby-on-rails-applications/9781801813785) book.\n\n\u003ca href=\"https://evilmartians.com/?utm_source=action_policy\"\u003e\n\u003cimg src=\"https://evilmartians.com/badges/sponsored-by-evil-martians.svg\" alt=\"Sponsored by Evil Martians\" width=\"236\" height=\"54\"\u003e\u003c/a\u003e\n\nRequirements:\n\n- Ruby ~\u003e 2.7\n- Rails 6+ (optional).\n\n**NOTE**: although most of the examples in this readme are Rails-specific, this gem could be used without Rails/ActiveSupport.\n\n## The problem\n\nWe need a way to handle different notifications _channel_ (mail, push) in one place.\n\nFrom the business-logic point of view, we want to _notify_ a user, hence we need a _separate abstraction layer_ as an entry point to different types of notifications.\n\n## The solution\n\nHere comes _Active Delivery_.\n\nIn the simplest case when we have only mailers Active Delivery is just a wrapper for Mailer with (possibly) some additional logic provided (e.g., preventing emails to unsubscribed users).\n\nMotivations behind Active Delivery:\n\n- Organize notifications-related logic:\n\n```ruby\n# Before\ndef after_some_action\n  MyMailer.with(user: user).some_action(resource).deliver_later if user.receive_emails?\n  NotifyService.send_notification(user, \"action\") if whatever_else?\nend\n\n# After\ndef after_some_action\n  MyDelivery.with(user: user).some_action(resource).deliver_later\nend\n```\n\n- Better testability (see [Testing](#testing)).\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem \"active_delivery\", \"~\u003e 1.0\"\n```\n\nAnd then execute:\n\n```sh\nbundle install\n```\n\n## Usage\n\nThe _Delivery_ class is used to trigger notifications. It describes how to notify a user (e.g., via email or push notification or both).\n\nFirst, it's recommended to create a base class for all deliveries with the configuration of the lines:\n\n```ruby\n# In the base class, you configure delivery lines\nclass ApplicationDelivery \u003c ActiveDelivery::Base\n  self.abstract_class = true\n\n  # Mailers are enabled by default, everything else must be declared explicitly\n\n  # For example, you can use a notifier line (see below) with a custom resolver\n  # (the argument is the delivery class)\n  register_line :sms, ActiveDelivery::Lines::Notifier,\n    resolver: -\u003e { _1.name.gsub(/Delivery$/, \"SMSNotifier\").safe_constantize } #=\u003e PostDelivery -\u003e PostSMSNotifier\n\n  # Or you can use a name pattern to resolve notifier classes for delivery classes\n  # Available placeholders are:\n  #  - delivery_class — full delivery class name\n  #  - delivery_name — full delivery class name without the \"Delivery\" suffix\n  register_line :webhook, ActiveDelivery::Lines::Notifier,\n    resolver_pattern: \"%{delivery_name}WebhookNotifier\" #=\u003e PostDelivery -\u003e PostWebhookNotifier\n\n  register_line :cable, ActionCableDeliveryLine\n  # and more\nend\n```\n\nThen, you can create a delivery class for a specific notification type. We follow Action Mailer conventions, and create a delivery class per resource:\n\n```ruby\nclass PostsDelivery \u003c ApplicationDelivery\nend\n```\n\nIn most cases, you just leave this class blank. The corresponding mailers, notifiers, etc., will be inferred automatically using the naming convention.\n\nYou don't need to define notification methods explicitly. Whenever you invoke a method on a delivery class, it will be proxied to the underlying _line handlers_ (mailers, notifiers, etc.):\n\n```ruby\nPostsDelivery.published(user, post).deliver_later\n\n# Under the hood it calls\nPostsMailer.published(user, post).deliver_later\nPostsSMSNotifier.published(user, post).notify_later\n\n# You can also pass options supported by your async executor (such as ActiveJob)\nPostsDelivery.published(user, post).deliver_later(wait_until: 1.day.from_now)\n\n# and whaterver your ActionCableDeliveryLine does\n# under the hood.\n```\n\nAlternatively, you call the `#notify` method with the notification name and the arguments:\n\n```ruby\nPostsDelivery.notify(:published, user, post)\n\n# Under the hood it calls\nPostsMailer.published(user, post).deliver_later\nPostsSMSNotifier.published(user, post).notify_later\n# ...\n```\n\nYou can also define a notification method explicitly if you want to add some logic:\n\n```ruby\nclass PostsDelivery \u003c ApplicationDelivery\n  def published(user, post)\n    # do something\n\n    # return a delivery object (to chain #deliver_later, etc.)\n    delivery(\n      notification: :published,\n      params: [user, post],\n      # For kwargs, you options\n      options: {},\n      # Metadata that can be used by line handlers\n      metadata: {}\n    )\n  end\nend\n```\n\nFinally, you can disable the default automatic proxying behaviour via the `ActiveDelivery.deliver_actions_required = true` configuration option. Then, in each delivery class, you can specify the available actions via the `.delivers` method:\n\n```ruby\nclass PostDelivery \u003c ApplicationDelivery\n  delivers :published\nend\n\nActiveDelivery.deliver_actions_required = true\n\nPostDelivery.published(post) #=\u003e ok\nPostDelivery.whatever(post) #=\u003e raises NoMethodError\n```\n\n### Organizing delivery and notifier classes\n\nThere are two common ways to organize delivery and notifier classes in your codebase:\n\n```txt\napp/\n  deliveries/                                 deliveries/\n    application_delivery.rb                     application_delivery.rb\n    post_delivery.rb                            post_delivery/\n    user_delivery.rb                              post_mailer.rb\n  mailers/                                        post_sms_notifier.rb\n    application_mailer.rb                         post_webhook_notifier.rb\n    post_mailer.rb                              post_delivery.rb\n    user_mailer.rb                              user_delivery/\n  notifiers/                                      user_mailer.rb\n    application_notifier.rb                       user_sms_notifier.rb\n    post_sms_notifier.rb                          user_webhook_notifier.rb\n    post_webhook_notifier.rb                    user_delivery.rb\n    user_sms_notifier.rb\n    user_webhook_notifier.rb\n```\n\nThe left side is a _flat_ structure, more typical for classic Rails applications. The right side follows the _sidecar pattern_ and aims to localize all the code related to a specific delivery class in a single directory. To use the sidecar version, you need to configure your delivery lines as follows:\n\n```ruby\nclass ApplicationDelivery \u003c ActiveDelivery::Base\n  self.abstract_class = true\n\n  register_line :mailer, ActiveDelivery::Lines::Mailer,\n    resolver_pattern: \"%{delivery_class}::%{delivery_name}_mailer\"\n  register_line :sms,\n    notifier: true,\n    resolver_pattern: \"%{delivery_class}::%{delivery_name}_sms_notifier\"\n  register_line :webhook,\n    notifier: true,\n    resolver_pattern: \"%{delivery_class}::%{delivery_name}_webhook_notifier\"\nend\n```\n\n### Customizing delivery handlers\n\nYou can specify a mailer class explicitly:\n\n```ruby\nclass PostsDelivery \u003c ActiveDelivery::Base\n  # You can pass a class name or a class itself\n  mailer \"CustomPostsMailer\"\n  # For other lines, you the line name as well\n  # sms \"MyPostsSMSNotifier\"\nend\n```\n\nOr you can provide a custom resolver by re-registering the line:\n\n```ruby\nclass PostsDelivery \u003c ActiveDelivery::Base\n  register_line :mailer, ActiveDelivery::Lines::Mailer, resolver: -\u003e(_delivery_class) { CustomMailer }\nend\n```\n\n### Parameterized deliveries\n\nDelivery also supports _parameterized_ calling:\n\n```ruby\nPostsDelivery.with(user: user).notify(:published, post)\n```\n\nThe parameters could be accessed through the `params` instance method (e.g., to implement guard-like logic).\n\n**NOTE**: When params are present, the parameterized mailer is used, i.e.:\n\n```ruby\nPostsMailer.with(user: user).published(post)\n```\n\nOther line implementations **MUST** also have the `#with` method in their public interface.\n\nSee [Rails docs](https://api.rubyonrails.org/classes/ActionMailer/Parameterized.html) for more information on parameterized mailers.\n\n### Callbacks support\n\n**NOTE:** callbacks are only available if ActiveSupport is present in the application's runtime.\n\n```ruby\n# Run method before delivering notification\n# NOTE: when `false` is returned the execution is halted\nbefore_notify :do_something\n\n# You can specify a notification line (to run callback only for that line)\nbefore_notify :do_mail_something, on: :mailer\n\n# You can specify a notification name (to run callback only for specific notification)\nafter_notify :mark_user_as_notified, only: %i[user_reminder]\n\n# if and unless options are also at your disposal\nafter_notify :mark_user_as_notified, if: -\u003e { params[:user].present? }\n\n# after_ and around_ callbacks are also supported\nafter_notify :cleanup\n\naround_notify :set_context\n\n# You can also skip callbacks in sub-classes\nskip_before_notify :do_something, only: %i[some_reminder]\n\n# NOTE: Specify `on` option for line-specific callbacks is required to skip them\nskip_after_notify :do_mail_something, on: :mailer\n```\n\nExample:\n\n```ruby\n# Let's log notifications\nclass MyDelivery \u003c ActiveDelivery::Base\n  after_notify do\n    # You can access the notification name within the instance\n    MyLogger.info \"Delivery triggered: #{notification_name}\"\n  end\nend\n\nMyDeliver.notify(:something_wicked_this_way_comes)\n#=\u003e Delivery triggered: something_wicked_this_way_comes\n```\n\n## Testing\n\n### Setup\n\nTest mode is activated automatically if `RAILS_ENV` or `RACK_ENV` env variable is equal to \"test\". Otherwise, add `require \"active_delivery/testing/rspec\"` to your `spec_helper.rb` / `rails_helper.rb` manually or `require \"active_delivery/testing/minitest\"`. This is also required if you're using Spring in the test environment (e.g. with help of [spring-commands-rspec](https://github.com/jonleighton/spring-commands-rspec)).\n\nFor Minitest, you also MUST include the test helper into your test class. For example:\n\n```ruby\nclass ActiveSupport::TestCase\n  # ...\n  include ActiveDelivery::TestHelper\nend\n```\n\n### Deliveries\n\nActive Delivery provides an elegant way to test deliveries in your code (i.e., when you want to check whether a notification has been sent) through a `have_delivered_to` RSpec matcher or `assert_delivery_enqueued` Minitest assertion:\n\n```ruby\n# RSpec\nit \"delivers notification\" do\n  expect { subject }.to have_delivered_to(Community::EventsDelivery, :modified, event)\n    .with(profile: profile)\nend\n\n# Minitest\ndef test_delivers_notification\n  assert_delivery_enqueued(Community::EventsDelivery, :modified, with: [event]) do\n    some_action\n  end\nend\n```\n\nYou can also use such RSpec features as compound expectations and composed matchers:\n\n```ruby\nit \"delivers to RSVPed members via .notify\" do\n  expect { subject }\n    .to have_delivered_to(Community::EventsDelivery, :canceled, an_instance_of(event)).with(\n      a_hash_including(profile: another_profile)\n    ).and have_delivered_to(Community::EventsDelivery, :canceled, event).with(\n      profile: profile\n    )\nend\n```\n\nIf you want to test that no notification is delivered you can use negation\n\n```ruby\n# RSpec\nspecify \"when event is not found\" do\n  expect do\n    described_class.perform_now(profile.id, \"123\", \"one_hour_before\")\n  end.not_to have_delivered_to(Community::EventsDelivery)\nend\n\n# Minitest\ndef test_no_notification_if_event_is_not_found\n  assert_no_deliveries do\n    some_action\n  end\n\n  # Alternatively, you can use the positive assertion\n  assert_deliveries(0) do\n    some_action\n  end\nend\n```\n\nWith RSpec, you can also use the `#have_not_delivered_to` matcher:\n\n```ruby\nspecify \"when event is not found\" do\n  expect do\n    described_class.perform_now(profile.id, \"123\", \"one_hour_before\")\n  end.to have_not_delivered_to(Community::EventsDelivery)\nend\n```\n\n### Delivery classes\n\nYou can test Delivery classes as regular Ruby classes:\n\n```ruby\ndescribe PostsDelivery do\n  let(:user) { build_stubbed(:user) }\n  let(:post) { build_stubbed(:post) }\n\n  describe \"#published\" do\n    it \"sends a mail\" do\n      expect {\n        described_class.published(user, post).deliver_now\n      }.to change { ActionMailer::Base.deliveries.count }.by(1)\n\n      mail = ActionMailer::Base.deliveries.last\n      expect(mail.to).to eq([user.email])\n      expect(mail.subject).to eq(\"New post published\")\n    end\n  end\nend\n```\n\nYou can also use the `#deliver_via` RSpec matcher as follows:\n\n```ruby\ndescribe PostsDelivery, type: :delivery do\n  let(:user) { build_stubbed(:user) }\n  let(:post) { build_stubbed(:post) }\n\n  describe \"#published\" do\n    it \"delivers to mailer and sms\" do\n      expect {\n        described_class.published(user, post).deliver_later\n      }.to deliver_via(:mailer, :sms)\n    end\n\n    context \"when user is not subscribed to SMS notifications\" do\n      let(:user) { build_stubbed(:user, sms_notifications: false) }\n\n      it \"delivers to mailer only\" do\n        expect {\n          described_class.published(user, post).deliver_now\n        }.to deliver_via(:mailer)\n      end\n    end\n  end\nend\n```\n\n## Custom \"lines\"\n\nThe _Line_ class describes the way you want to _transfer_ your deliveries.\n\nWe only provide only Action Mailer _line_ out-of-the-box.\n\nA line connects _delivery_ to the _sender_ class responsible for sending notifications.\n\nIf you want to use parameterized deliveries, your _sender_ class must respond to `.with(params)` method.\n\n### A full-featured line example: pigeons 🐦\n\nAssume that we want to send messages via _pigeons_ and we have the following sender class:\n\n```ruby\nclass EventPigeon\n  class \u003c\u003c self\n    # Add `.with`  method as an alias\n    alias_method :with, :new\n\n    # delegate delivery action to the instance\n    def message_arrived(*)\n      new.message_arrived(*)\n    end\n  end\n\n  def initialize(params = {})\n    # do smth with params\n  end\n\n  def message_arrived(msg)\n    # send a pigeon with the message\n  end\nend\n```\n\nNow we want to add a _pigeon_ line to our `EventDelivery,` that is we want to send pigeons when\nwe call `EventDelivery.notify(:message_arrived, \"ping-pong!\")`.\n\nLine class has the following API:\n\n```ruby\nclass PigeonLine \u003c ActiveDelivery::Lines::Base\n  # This method is used to infer sender class\n  # `name` is the name of the delivery class\n  def resolve_class(name)\n    name.gsub(/Delivery$/, \"Pigeon\").safe_constantize\n  end\n\n  # This method should return true if the sender recognizes the delivery action\n  def notify?(delivery_action)\n    # `handler_class` is available within the line instance\n    sender_class.respond_to?(delivery_action)\n  end\n\n  # Called when we want to send message synchronously\n  # `sender` here either `sender_class` or `sender_class.with(params)`\n  # if params passed.\n  def notify_now(sender, delivery_action, *, **)\n    # For example, our EventPigeon class returns some `Pigeon` object\n    pigeon = sender.public_send(delivery_action, *, **)\n    # PigeonLaunchService do all the sending job\n    PigeonService.launch pigeon\n  end\n\n  # Called when we want to send a message asynchronously.\n  # For example, you can use a background job here.\n  def notify_later(sender, delivery_action, *, **)\n    pigeon = sender.public_send(delivery_action, *, **)\n    # PigeonLaunchService do all the sending job\n    PigeonLaunchJob.perform_later pigeon\n  end\nend\n```\n\nIn the case of parameterized calling, some update needs to be done on the new Line. Here is an example:\n\n```ruby\nclass EventPigeon\n  attr_reader :params\n\n  class \u003c\u003c self\n    # Add `.with`  method as an alias\n    alias_method :with, :new\n\n    # delegate delivery action to the instance\n    def message_arrived(*)\n      new.message_arrived(*)\n    end\n  end\n\n  def initialize(params = {})\n    @params = params\n    # do smth with params\n  end\n\n  def message_arrived(msg)\n    # send a pigeon with the message\n  end\nend\n\nclass PigeonLine \u003c ActiveDelivery::Lines::Base\n  def notify_later(sender, delivery_action, *, **kwargs)\n    # `to_s` is important for serialization. Unless you might have error\n    PigeonLaunchJob.perform_later(sender.class.to_s, delivery_action, *, **kwargs.merge(params: line.params))\n  end\nend\n\nclass PigeonLaunchJob \u003c ActiveJob::Base\n  def perform(sender, delivery_action, *, params: nil, **)\n    klass = sender.safe_constantize\n    handler = params ? klass.with(**params) : klass.new\n\n    handler.public_send(delivery_action, *, **)\n  end\nend\n```\n\n**NOTE**: we fall back to the superclass's sender class if `resolve_class` returns nil.\nYou can disable automatic inference of sender classes by marking delivery as _abstract_:\n\n```ruby\n# we don't want to use ApplicationMailer by default, don't we?\nclass ApplicationDelivery \u003c ActiveDelivery::Base\n  self.abstract_class = true\nend\n```\n\nThe final step is to register the line within your delivery class:\n\n```ruby\nclass EventDelivery \u003c ActiveDelivery::Base\n  # under the hood a new instance of PigeonLine is created\n  # and used to send pigeons!\n  register_line :pigeon, PigeonLine\n\n  # you can pass additional options to customize your line\n  # (and use multiple pigeons lines with different configuration)\n  #\n  # register_line :pigeon, PigeonLine, namespace: \"AngryPigeons\"\n  #\n  # now you can explicitly specify pigeon class\n  # pigeon \"MyCustomPigeon\"\n  #\n  # or define pigeon specific callbacks\n  #\n  # before_notify :ensure_pigeon_is_not_dead, on: :pigeon\nend\n```\n\nYou can also _unregister_ a line:\n\n```ruby\nclass NonMailerDelivery \u003c ActiveDelivery::Base\n  # Use unregister_line to remove any default or inherited lines\n  unregister_line :mailer\nend\n```\n\n### An example of a universal sender: Action Cable\n\nAlthough Active Delivery is designed to work with Action Mailer-like abstraction, it's flexible enough to support other use cases.\n\nFor example, for some notification channels, we don't need to create a separate class for each resource or context; we can send the payload right to the communication channel. Let's consider an Action Cable line as an example.\n\nFor every delivery, we want to broadcast a message via Action Cable to the stream corresponding to the delivery class name. For example:\n\n```ruby\n# Our PostsDelivery example from the beginning\nPostsDelivery.with(user:).notify(:published, post)\n\n# Will results in the following Action Cable broadcast:\nDeliveryChannel.broadcast_to user, {event: \"posts.published\", post_id: post.id}\n```\n\nThe `ActionCableDeliveryLine` class can be implemented as follows:\n\n```ruby\nclass ActionCableDeliveryLine \u003c ActiveDelivery::Line::Base\n  # Context is our universal sender.\n  class Context\n    attr_reader :user\n\n    def initialize(scope)\n      @scope = scope\n    end\n\n    # User is required for this line\n    def with(user:, **)\n      @user = user\n      self\n    end\n  end\n\n  # The result of this callback is passed further to the `notify_now` method\n  def resolve_class(name)\n    Context.new(name.sub(/Delivery$/, \"\").underscore)\n  end\n\n  # We want to broadcast all notifications\n  def notify?(...) = true\n\n  def notify_now(context, delivery_action, *, **)\n    # Skip if no user provided\n    return unless context.user\n\n    payload = {event: [context.scope, delivery_action].join(\".\")}\n    payload.merge!(serialized_args(*, **))\n\n    DeliveryChannel.broadcast_to context.user, payload\n  end\n\n  # Broadcasts are asynchronous by nature, so we can just use `notify_now`\n  alias_method :notify_later, :notify_now\n\n  private\n\n  def serialized_args(*args, **kwargs)\n    # Code that convers AR objects into IDs, etc.\n  end\nend\n```\n\n## Abstract Notifier\n\nAbstract Notifier is a tool that allows you to describe/model any text-based notifications (such as Push Notifications) the same way Action Mailer does for email notifications.\n\nAbstract Notifier (as the name states) doesn't provide any specific implementation for sending notifications. Instead, it offers tools to organize your notification-specific code and make it easily testable.\n\n### Notifier classes\n\nA **notifier object** is very similar to an Action Mailer's mailer with  the `#notification` method used instead of the `#mail` method:\n\n```ruby\nclass EventsNotifier \u003c ApplicationNotifier\n  def canceled(profile, event)\n    notification(\n      # the only required option is `body`\n      body: \"Event #{event.title} has been canceled\",\n      # all other options are passed to delivery driver\n      identity: profile.notification_service_id\n    )\n  end\nend\n\n# send notification later\nEventsNotifier.canceled(profile, event).notify_later\n\n# or immediately\nEventsNotifier.canceled(profile, event).notify_now\n```\n\n### Delivery drivers\n\nTo perform actual deliveries you **must** configure a _delivery driver_:\n\n```ruby\nclass ApplicationNotifier \u003c AbstractNotifier::Base\n  self.driver = MyFancySender.new\nend\n```\n\nA driver could be any callable Ruby object (i.e., anything that responds to `#call`).\n\nThat's the developer's responsibility to implement the driver (we do not provide any drivers out-of-the-box; at least yet).\n\nYou can set different drivers for different notifiers.\n\n### Parameterized notifiers\n\nAbstract Notifier supports parameterization the same way as [Action Mailer](https://api.rubyonrails.org/classes/ActionMailer/Parameterized.html):\n\n```ruby\nclass EventsNotifier \u003c ApplicationNotifier\n  def canceled(event)\n    notification(\n      body: \"Event #{event.title} has been canceled\",\n      identity: params[:profile].notification_service_id\n    )\n  end\nend\n\nEventsNotifier.with(profile: profile).canceled(event).notify_later\n```\n\n### Defaults\n\nYou can specify default notification fields at a class level:\n\n```ruby\nclass EventsNotifier \u003c ApplicationNotifier\n  # `category` field will be added to the notification\n  # if missing\n  default category: \"EVENTS\"\n\n  # ...\nend\n```\n\n**NOTE**: when subclassing notifiers, default parameters are merged.\n\nYou can also specify a block or a method name as the default params _generator_.\nThis could be useful in combination with the `#notification_name` method to generate dynamic payloads:\n\n```ruby\nclass ApplicationNotifier \u003c AbstractNotifier::Base\n  default :build_defaults_from_locale\n\n  private\n\n  def build_defaults_from_locale\n    {\n      subject: I18n.t(notification_name, scope: [:notifiers, self.class.name.underscore])\n    }\n  end\nend\n```\n\n### Background jobs / async notifications\n\nTo use `#notify_later(**delivery_options)` you **must** configure an async adapter for Abstract Notifier.\n\nWe provide an Active Job adapter out of the box and enable it if Active Job is found.\n\nA custom async adapter must implement the `#enqueue` method:\n\n```ruby\nclass MyAsyncAdapter\n  # adapters may accept options\n  def initialize(options = {})\n  end\n\n  # `enqueue_delivery` method accepts notifier class, action name and notification parameters\n  def enqueue_delivery(delivery, **options)\n    # \u003cYour implementation here\u003e\n    # To trigger the notification delivery, you can use the following snippet:\n    #\n    #   AbstractNotifier::NotificationDelivery.new(\n    #     delivery.notifier_class, delivery.action_name, **delivery.delivery_params\n    #   ).notify_now\n  end\nend\n\n# Configure globally\nAbstractNotifier.async_adapter = MyAsyncAdapter.new\n\n# or per-notifier\nclass EventsNotifier \u003c AbstractNotifier::Base\n  self.async_adapter = MyAsyncAdapter.new\nend\n```\n\n### Action and Delivery Callbacks\n\n**NOTE:** callbacks are only available if ActiveSupport is present in the application's runtime.\n\n```ruby\n# Run method before building a notification payload\n# NOTE: when `false` is returned the execution is halted\nbefore_action :do_something\n\n# Run method before delivering notification\n# NOTE: when `false` is returned the execution is halted\nbefore_deliver :do_something\n\n# Run method after the notification payload was build but before delivering\nafter_action :verify_notification_payload\n\n# Run method after the actual delivery was performed\nafter_deliver :mark_user_as_notified, if: -\u003e { params[:user].present? }\n\n# after_ and around_ callbacks are also supported\nafter_action_ :cleanup\n\naround_deliver :set_context\n\n# You can also skip callbacks in sub-classes\nskip_before_action :do_something, only: %i[some_reminder]\n```\n\nExample:\n\n```ruby\nclass MyNotifier \u003c AbstractNotifier::Base\n  # Log sent notifications\n  after_deliver do\n    # You can access the notification name within the instance or\n    MyLogger.info \"Notification sent: #{notification_name}\"\n  end\n\n  def some_event(body)\n    notification(body:)\n  end\nend\n\nMyNotifier.some_event(\"hello\")\n#=\u003e Notification sent: some_event\n```\n\n### Delivery modes\n\nFor test/development purposes there are two special _global_ delivery modes:\n\n```ruby\n# Track all sent notifications without peforming real actions.\n# Required for using RSpec matchers.\n#\n# config/environments/test.rb\nAbstractNotifier.delivery_mode = :test\n\n# If you don't want to trigger notifications in development,\n# you can make Abstract Notifier no-op.\n#\n# config/environments/development.rb\nAbstractNotifier.delivery_mode = :noop\n\n# Default delivery mode is \"normal\"\nAbstractNotifier.delivery_mode = :normal\n```\n\n**NOTE:** we set `delivery_mode = :test` if `RAILS_ENV` or `RACK_ENV` env variable is equal to \"test\".\nOtherwise add `require \"abstract_notifier/testing\"` to your `spec_helper.rb` / `rails_helper.rb` manually.\n\n**NOTE:** delivery mode affects all drivers.\n\n### Testing notifier deliveries\n\nAbstract Notifier provides two convenient RSpec matchers:\n\n```ruby\n# for testing sync notifications (sent with `notify_now`)\nexpect { EventsNotifier.with(profile: profile).canceled(event).notify_now }\n  .to have_sent_notification(identify: \"123\", body: \"Alarma!\")\n\n# for testing async notifications (sent with `notify_later`)\nexpect { EventsNotifier.with(profile: profile).canceled(event).notify_later }\n  .to have_enqueued_notification(via: EventNotifier, identify: \"123\", body: \"Alarma!\")\n\n# you can also specify the expected notifier class (useful when ypu have multiple notifier lines)\nexpect { EventsNotifier.with(profile: profile).canceled(event).notify_now }\n  .to have_sent_notification(via: EventsNotifier, identify: \"123\", body: \"Alarma!\")\n```\n\nAbstract Notifier also provides Minitest assertions:\n\n```ruby\nrequire \"abstract_notifier/testing/minitest\"\n\nclass EventsNotifierTestCase \u003c Minitest::Test\n  include AbstractNotifier::TestHelper\n\n  test \"canceled\" do\n    assert_notifications_sent 1, identify: \"321\", body: \"Alarma!\" do\n      EventsNotifier.with(profile: profile).canceled(event).notify_now\n    end\n\n    assert_notifications_sent 1, via: EventNofitier, identify: \"123\", body: \"Alarma!\" do\n      EventsNotifier.with(profile: profile).canceled(event).notify_now\n    end\n\n    assert_notifications_enqueued 1, via: EventNofitier, identify: \"123\", body: \"Alarma!\" do\n      EventsNotifier.with(profile: profile).canceled(event).notify_later\n    end\n  end\nend\n```\n\n**NOTE:** test mode activated automatically if `RAILS_ENV` or `RACK_ENV` env variable is equal to \"test\". Otherwise, add `require \"abstract_notifier/testing/rspec\"` to your `spec_helper.rb` / `rails_helper.rb` manually. This is also required if you're using Spring in a test environment (e.g. with help of [spring-commands-rspec](https://github.com/jonleighton/spring-commands-rspec)).\n\n### Notifier lines for Active Delivery\n\nAbstract Notifier provides a _notifier_ line for Active Delivery:\n\n```ruby\nclass ApplicationDelivery \u003c ActiveDelivery::Base\n  # Add notifier line to you delivery\n  # By default, we use `*Delivery` -\u003e `*Notifier` resolution mechanism\n  register_line :notifier, notifier: true\n\n  # You can define a custom suffix to use for notifier classes:\n  #   `*Delivery` -\u003e `*CustomNotifier`\n  register_line :custom_notifier, notifier: true, suffix: \"CustomNotifier\"\n\n  # Or using a custom pattern\n  register_line :custom_notifier, notifier: true, resolver_pattern: \"%{delivery_name}CustomNotifier\"\n\n  # Or you can specify a Proc object to do custom resolution:\n  register_line :some_notifier, notifier: true,\n    resolver: -\u003e(delivery_class) { resolve_somehow(delivery_class) }\nend\n```\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/palkan/active_delivery.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpalkan%2Factive_delivery","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpalkan%2Factive_delivery","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpalkan%2Factive_delivery/lists"}