vinistock/correspondent

View on GitHub
README.md

Summary

Maintainability
Test Coverage
This unfinished experiment is no longer active. This was never actually ready and really shouldn't be used.

# Correspondent

Dead simple configurable user notifications using the Correspondent engine!

Configure subscribers and publishers and let Correspondent deal with all notification work with very little overhead.

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'correspondent'
```

And then execute:

```bash
$ bundle
```

Create the necessary migrations:

```bash
$ rails g correspondent:install
```

## Usage

### Model configuration

Notifications can easily be setup using Correspondent. The following example goes through the basic usage. There are only two steps for the basic configuration:

1. Invoke notifies and configure the subscriber (user in this case), the triggers (method purchase in this case) and desired options
2. Define the to_notification method to configure information that needs to be used to create notifications

```ruby
# Example model using Correspondent
# app/models/purchase.rb
class Purchase < ApplicationRecord
  belongs_to :user
  belongs_to :store

  # Notifies configuration
  # First argument is the subscriber (the one that receives a notification). Can be an NxN association as well (e.g.: users) which will create a notification for each associated record.
  # Second argument are the triggers (the method inside that model that triggers notifications). Can be an array of symbols for multiple triggers for the same entity.
  # Third argument are generic options as a hash
  notifies :user, :purchase, avoid_duplicates: true

  # Many notifies definitions can be used for different subscribers
  # In the following case, every time purchase is invoked the following will happen:
  # 1. A notification will be created for `user`
  # 2. A notification will be created for `store`
  # 3. An email will be triggered using the `StoreMailer` (invoking a method called purchase_email)
  notifies :store, :purchase, avoid_duplicates: true, mailer: StoreMailer

  # `notifies` will hook into the desired triggers.
  # Every time this method is invoked by an instance of Purchase
  # a notification will be created in the database using the
  # `to_notification` method. The handling of notifications is
  # done asynchronously to cause as little overhead as possible.
  def purchase
    # some business logic
  end

  # The to_notification method returns the information to be
  # used for creating a notification. This will be invoked automatically
  # by the gem when a trigger occurs.
  # When calling this method, entity and trigger will be passed. Entity
  # is the subscriber (in this example, `user`). Trigger is the method
  # that triggered the notification. With this approach, the hash
  # built to pass information can vary based on different triggers.
  # If entity and trigger will not be used, this can simply be defined as
  #
  # def to_notification(*)
  #   # some hash
  # end
  def to_notification(entity:, trigger:)
    {
      title: "Purchase ##{id} for #{entity} #{send(entity).name}",
      content: "Congratulations on your recent #{trigger} of #{name}",
      image_url: "",
      link_url: "/purchases/#{id}",
      referrer_url: "/stores/#{store.id}"
    }
  end
end
```

Correspondent can also trigger emails if desired. To trigger emails, the mailer class should be passed as an object and should implement a method follwing the naming convention.

```ruby
# app/models/purchase.rb

class Purchase < ApplicationRecord
  belongs_to :user

  # Pass the desired mailer in the `mailer:` option
  notifies :user, :purchase, mailer: ApplicationMailer

  def purchase
    # some business logic
  end
end

# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: 'from@example.com'
  layout 'mailer'

  # The mailer should implement methods following the naming convention of
  # #{trigger}_email(triggering_instance)
  #
  # In this case, the `trigger` is the method purchase, so Correspondent will look for
  # the purchase_email method. It will always pass the instance that triggered the email
  # as an argument.
  def purchase_email(purchase)
    @purchase = purchase
    mail(to: purchase.user.email, subject: "Congratulations on the purchase of #{purchase.name}")
  end
end
```

To reference the created notifications in the desired model, use the following association:

```ruby
# app/models/purchase.rb

class User < ApplicationRecord
  has_many :purchases
  has_many :notifications, class_name: "Correspondent::Notification", as: :subscriber
end

class Purchase < ApplicationRecord
  belongs_to :user
  has_many :notifications, class_name: "Correspondent::Notification", as: :publisher
end
```

If a specific column is not needed for your project, remove them from the generated migrations and don't return the respective attribute inside the to_notification method.

### Options

The available options, their default values and their explanations are listed below.

```ruby
# Avoid duplicates
# Prevents creating new notifications if a non dismissed notification for the same publisher and same subscriber already exists
notifies :some_resouce, :trigger, avoid_duplicates: false

# Mailer
# The Mailer class that implements the desired mailer triggers to send emails. Default is nil (doesn't send emails).
notifies :some_resouce, :trigger, mailer: nil

# Email only
# For preventing the creation of notifications and only trigger emails, add the email_only option
notifies :some_resouce, :trigger, email_only: false

# Conditionals
# If or unless options can be passed either as procs/lambdas or symbols representing the name of a method
# These will be evaluated in an instance context, every time trigger is invoked
notifies :some_resource, :trigger, if: :should_be_notified?

notifies :some_resource, :trigger, unless: -> { should_be_notified? && is_eligible? }
```

### JSON API

Correspondent exposes a few APIs to be used for handling notification logic in the application.

All APIs use the `stale?` check. So if passing the If-None-Match header, the API will support returning 304 (not modified) if the collection hasn't changed.

```json
Parameters

:subscriber_type -> The subscriber resource name - not in plural (e.g.: user)
:subscriber_id   -> The id of the subscriber

Index

Retrieves all non dismissed notifications for a given subscriber.

Request
GET /correspondent/:subscriber_type/:subscriber_id/notifications

Response
[
    {
        "id":20,
        "title":"Purchase #1 for user user",
        "content":"Congratulations on your recent purchase of purchase",
        "image_url":"",
        "dismissed":false,
        "publisher_type":"Purchase",
        "publisher_id":1,
        "created_at":"2019-03-01T14:19:31.273Z",
        "link_url":"/purchases/1",
        "referrer_url":"/stores/1"
    }
]

Preview

Returns total number of non dismissed notifications and the newest notification.

Request
GET /correspondent/:subscriber_type/:subscriber_id/notifications/preview

Response
{
    "count": 3,
    "notification": {
        "id":20,
        "title":"Purchase #1 for user user",
        "content":"Congratulations on your recent purchase of purchase",
        "image_url":"",
        "dismissed":false,
        "publisher_type":"Purchase",
        "publisher_id":1,
        "created_at":"2019-03-01T14:22:31.649Z",
        "link_url":"/purchases/1",
        "referrer_url":"/stores/1"
    }
}


Dismiss

Dismisses a given notification.

Resquest
PUT /correspondent/:subscriber_type/:subscriber_id/notifications/:notification_id/dismiss

Response
STATUS no_content (204)

Destroy

Destroys a given notification.

Resquest
DELETE /correspondent/:subscriber_type/:subscriber_id/notifications/:notification_id

Response
STATUS no_content (204)
```

## Contributing

Contributions are very welcome! Don't hesitate to ask if you wish to contribute, but don't yet know how. Please refer to this simple [guideline].

[guideline]: https://github.com/vinistock/correspondent/blob/master/CONTRIBUTING.md