correspondent 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ed9caf9e54953e0f3dba66aa26746a76ac5acaf5d8921b5390f9614b48e2ccb1
4
+ data.tar.gz: 24f21fb0868aabed63abfb1c04fa24820d259161761d96cccbeda1d787e1c166
5
+ SHA512:
6
+ metadata.gz: 2e55c96ecf807357426baec8ed78c20ceabd900e38e239efa0fd0896a4565036992536038d00f0e59ceed27294952c7f5f2cdca613ff37333221d49eb7835927
7
+ data.tar.gz: a1ecdedc87db4a93cdee1717fdadc1d970e1e61e90070664f763a38935a6768326ef1430f457bf970395a9c23054bb2a1a623da066f28d2299f513e7963cb480
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2019 Vinicius Stock
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,253 @@
1
+ This unfinished experiment is no longer active.
2
+
3
+ # Correspondent
4
+
5
+ Dead simple configurable user notifications using the Correspondent engine!
6
+
7
+ Configure subscribers and publishers and let Correspondent deal with all notification work with very little overhead.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'correspondent'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ ```bash
20
+ $ bundle
21
+ ```
22
+
23
+ Create the necessary migrations:
24
+
25
+ ```bash
26
+ $ rails g correspondent:install
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ### Model configuration
32
+
33
+ Notifications can easily be setup using Correspondent. The following example goes through the basic usage. There are only two steps for the basic configuration:
34
+
35
+ 1. Invoke notifies and configure the subscriber (user in this case), the triggers (method purchase in this case) and desired options
36
+ 2. Define the to_notification method to configure information that needs to be used to create notifications
37
+
38
+ ```ruby
39
+ # Example model using Correspondent
40
+ # app/models/purchase.rb
41
+ class Purchase < ApplicationRecord
42
+ belongs_to :user
43
+ belongs_to :store
44
+
45
+ # Notifies configuration
46
+ # 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.
47
+ # 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.
48
+ # Third argument are generic options as a hash
49
+ notifies :user, :purchase, avoid_duplicates: true
50
+
51
+ # Many notifies definitions can be used for different subscribers
52
+ # In the following case, every time purchase is invoked the following will happen:
53
+ # 1. A notification will be created for `user`
54
+ # 2. A notification will be created for `store`
55
+ # 3. An email will be triggered using the `StoreMailer` (invoking a method called purchase_email)
56
+ notifies :store, :purchase, avoid_duplicates: true, mailer: StoreMailer
57
+
58
+ # `notifies` will hook into the desired triggers.
59
+ # Every time this method is invoked by an instance of Purchase
60
+ # a notification will be created in the database using the
61
+ # `to_notification` method. The handling of notifications is
62
+ # done asynchronously to cause as little overhead as possible.
63
+ def purchase
64
+ # some business logic
65
+ end
66
+
67
+ # The to_notification method returns the information to be
68
+ # used for creating a notification. This will be invoked automatically
69
+ # by the gem when a trigger occurs.
70
+ # When calling this method, entity and trigger will be passed. Entity
71
+ # is the subscriber (in this example, `user`). Trigger is the method
72
+ # that triggered the notification. With this approach, the hash
73
+ # built to pass information can vary based on different triggers.
74
+ # If entity and trigger will not be used, this can simply be defined as
75
+ #
76
+ # def to_notification(*)
77
+ # # some hash
78
+ # end
79
+ def to_notification(entity:, trigger:)
80
+ {
81
+ title: "Purchase ##{id} for #{entity} #{send(entity).name}",
82
+ content: "Congratulations on your recent #{trigger} of #{name}",
83
+ image_url: "",
84
+ link_url: "/purchases/#{id}",
85
+ referrer_url: "/stores/#{store.id}"
86
+ }
87
+ end
88
+ end
89
+ ```
90
+
91
+ 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.
92
+
93
+ ```ruby
94
+ # app/models/purchase.rb
95
+
96
+ class Purchase < ApplicationRecord
97
+ belongs_to :user
98
+
99
+ # Pass the desired mailer in the `mailer:` option
100
+ notifies :user, :purchase, mailer: ApplicationMailer
101
+
102
+ def purchase
103
+ # some business logic
104
+ end
105
+ end
106
+
107
+ # app/mailers/application_mailer.rb
108
+ class ApplicationMailer < ActionMailer::Base
109
+ default from: 'from@example.com'
110
+ layout 'mailer'
111
+
112
+ # The mailer should implement methods following the naming convention of
113
+ # #{trigger}_email(triggering_instance)
114
+ #
115
+ # In this case, the `trigger` is the method purchase, so Correspondent will look for
116
+ # the purchase_email method. It will always pass the instance that triggered the email
117
+ # as an argument.
118
+ def purchase_email(purchase)
119
+ @purchase = purchase
120
+ mail(to: purchase.user.email, subject: "Congratulations on the purchase of #{purchase.name}")
121
+ end
122
+ end
123
+ ```
124
+
125
+ To reference the created notifications in the desired model, use the following association:
126
+
127
+ ```ruby
128
+ # app/models/purchase.rb
129
+
130
+ class User < ApplicationRecord
131
+ has_many :purchases
132
+ has_many :notifications, class_name: "Correspondent::Notification", as: :subscriber
133
+ end
134
+
135
+ class Purchase < ApplicationRecord
136
+ belongs_to :user
137
+ has_many :notifications, class_name: "Correspondent::Notification", as: :publisher
138
+ end
139
+ ```
140
+
141
+ 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.
142
+
143
+ ### Options
144
+
145
+ The available options, their default values and their explanations are listed below.
146
+
147
+ ```ruby
148
+ # Avoid duplicates
149
+ # Prevents creating new notifications if a non dismissed notification for the same publisher and same subscriber already exists
150
+ notifies :some_resouce, :trigger, avoid_duplicates: false
151
+
152
+ # Mailer
153
+ # The Mailer class that implements the desired mailer triggers to send emails. Default is nil (doesn't send emails).
154
+ notifies :some_resouce, :trigger, mailer: nil
155
+
156
+ # Email only
157
+ # For preventing the creation of notifications and only trigger emails, add the email_only option
158
+ notifies :some_resouce, :trigger, email_only: false
159
+
160
+ # Conditionals
161
+ # If or unless options can be passed either as procs/lambdas or symbols representing the name of a method
162
+ # These will be evaluated in an instance context, every time trigger is invoked
163
+ notifies :some_resource, :trigger, if: :should_be_notified?
164
+
165
+ notifies :some_resource, :trigger, unless: -> { should_be_notified? && is_eligible? }
166
+ ```
167
+
168
+ ### JSON API
169
+
170
+ Correspondent exposes a few APIs to be used for handling notification logic in the application.
171
+
172
+ 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.
173
+
174
+ ```json
175
+ Parameters
176
+
177
+ :subscriber_type -> The subscriber resource name - not in plural (e.g.: user)
178
+ :subscriber_id -> The id of the subscriber
179
+
180
+ Index
181
+
182
+ Retrieves all non dismissed notifications for a given subscriber.
183
+
184
+ Request
185
+ GET /correspondent/:subscriber_type/:subscriber_id/notifications
186
+
187
+ Response
188
+ [
189
+ {
190
+ "id":20,
191
+ "title":"Purchase #1 for user user",
192
+ "content":"Congratulations on your recent purchase of purchase",
193
+ "image_url":"",
194
+ "dismissed":false,
195
+ "publisher_type":"Purchase",
196
+ "publisher_id":1,
197
+ "created_at":"2019-03-01T14:19:31.273Z",
198
+ "link_url":"/purchases/1",
199
+ "referrer_url":"/stores/1"
200
+ }
201
+ ]
202
+
203
+ Preview
204
+
205
+ Returns total number of non dismissed notifications and the newest notification.
206
+
207
+ Request
208
+ GET /correspondent/:subscriber_type/:subscriber_id/notifications/preview
209
+
210
+ Response
211
+ {
212
+ "count": 3,
213
+ "notification": {
214
+ "id":20,
215
+ "title":"Purchase #1 for user user",
216
+ "content":"Congratulations on your recent purchase of purchase",
217
+ "image_url":"",
218
+ "dismissed":false,
219
+ "publisher_type":"Purchase",
220
+ "publisher_id":1,
221
+ "created_at":"2019-03-01T14:22:31.649Z",
222
+ "link_url":"/purchases/1",
223
+ "referrer_url":"/stores/1"
224
+ }
225
+ }
226
+
227
+
228
+ Dismiss
229
+
230
+ Dismisses a given notification.
231
+
232
+ Resquest
233
+ PUT /correspondent/:subscriber_type/:subscriber_id/notifications/:notification_id/dismiss
234
+
235
+ Response
236
+ STATUS no_content (204)
237
+
238
+ Destroy
239
+
240
+ Destroys a given notification.
241
+
242
+ Resquest
243
+ DELETE /correspondent/:subscriber_type/:subscriber_id/notifications/:notification_id
244
+
245
+ Response
246
+ STATUS no_content (204)
247
+ ```
248
+
249
+ ## Contributing
250
+
251
+ 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].
252
+
253
+ [guideline]: https://github.com/vinistock/correspondent/blob/master/CONTRIBUTING.md
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "bundler/setup"
5
+ rescue LoadError
6
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
7
+ end
8
+
9
+ require "rdoc/task"
10
+
11
+ RDoc::Task.new(:rdoc) do |rdoc|
12
+ rdoc.rdoc_dir = "rdoc"
13
+ rdoc.title = "Correspondent"
14
+ rdoc.options << "--line-numbers"
15
+ rdoc.rdoc_files.include("README.md")
16
+ rdoc.rdoc_files.include("lib/**/*.rb")
17
+ end
18
+
19
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
20
+ load "rails/tasks/engine.rake"
21
+ load "rails/tasks/statistics.rake"
22
+
23
+ require "bundler/gem_tasks"
24
+ require "rake/testtask"
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << "test"
28
+ t.pattern = Dir["test/**/*_test.rb"].reject { |path| path.include?("benchmarks") }
29
+ t.verbose = false
30
+ end
31
+
32
+ task default: :test
33
+
34
+ namespace :test do
35
+ Rake::TestTask.new(:benchmark) do |t|
36
+ t.libs << "test"
37
+ t.pattern = "test/benchmarks/**/*_test.rb"
38
+ t.verbose = false
39
+ end
40
+ end
41
+
42
+ task all: :environment do
43
+ system(
44
+ "brakeman && "\
45
+ "rake && "\
46
+ "rubocop --auto-correct && "\
47
+ "rails_best_practices"
48
+ )
49
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Correspondent
4
+ class ApplicationController < ActionController::API # :nodoc:
5
+ end
6
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "correspondent/application_controller"
4
+
5
+ module Correspondent
6
+ # NotificationsController
7
+ #
8
+ # API for all notifications related
9
+ # endpoints.
10
+ class NotificationsController < ApplicationController
11
+ before_action :find_notification, only: %i[dismiss destroy]
12
+
13
+ # index
14
+ #
15
+ # Returns all notifications for a given subscriber.
16
+ def index
17
+ notifications = Correspondent::Notification.for_subscriber(params[:subscriber_type], params[:subscriber_id])
18
+ render(json: notifications) if stale?(notifications)
19
+ end
20
+
21
+ # preview
22
+ #
23
+ # Returns the newest notification and the total
24
+ # number of notifications for the given subscriber.
25
+ def preview
26
+ notifications = Correspondent::Notification.for_subscriber(params[:subscriber_type], params[:subscriber_id])
27
+
28
+ if stale?(notifications)
29
+ render(
30
+ json: {
31
+ count: notifications.count,
32
+ notification: notifications.limit(1).first
33
+ }
34
+ )
35
+ end
36
+ end
37
+
38
+ # dismiss
39
+ #
40
+ # Dismisses a given notification.
41
+ def dismiss
42
+ @notification&.dismiss!
43
+ head(:no_content)
44
+ end
45
+
46
+ # destroy
47
+ #
48
+ # Destroys a given notification.
49
+ def destroy
50
+ @notification&.destroy
51
+ head(:no_content)
52
+ end
53
+
54
+ private
55
+
56
+ def find_notification
57
+ @notification = Correspondent::Notification
58
+ .select(:id, :subscriber_type, :subscriber_id, :publisher_id, :publisher_type)
59
+ .find_by(id: params[:id])
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Correspondent
4
+ class ApplicationJob < ActiveJob::Base # :nodoc:
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Correspondent
4
+ class ApplicationRecord < ActiveRecord::Base # :nodoc:
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Correspondent
4
+ # Notification
5
+ #
6
+ # Model to hold all notification logic.
7
+ class Notification < ApplicationRecord
8
+ belongs_to :subscriber, polymorphic: true
9
+ belongs_to :publisher, polymorphic: true
10
+
11
+ validates :publisher, :subscriber, presence: true
12
+ before_destroy :delete_cache_entry
13
+
14
+ scope :not_dismissed, -> { where(dismissed: false) }
15
+ scope :by_parents, lambda { |subscriber, publisher|
16
+ select(:id)
17
+ .where(subscriber: subscriber, publisher: publisher)
18
+ .not_dismissed
19
+ }
20
+
21
+ scope :for_subscriber, lambda { |type, id|
22
+ not_dismissed
23
+ .where(subscriber_type: type.capitalize, subscriber_id: id)
24
+ .order(id: :desc)
25
+ }
26
+
27
+ class << self
28
+ # create_for!
29
+ #
30
+ # Creates notification(s) for the given
31
+ # +instance+ of the publisher and given
32
+ # +entity+ (subscriber).
33
+ def create_for!(attrs, options = {})
34
+ attributes = attrs[:instance].to_notification(entity: attrs[:entity], trigger: attrs[:trigger])
35
+ attributes[:publisher] = attrs[:instance]
36
+
37
+ relation = attrs[:instance].send(attrs[:entity])
38
+
39
+ if relation.respond_to?(:each)
40
+ create_many!(attributes, relation, options)
41
+ else
42
+ create_single!(attributes, relation, options)
43
+ end
44
+ end
45
+
46
+ # create_many!
47
+ #
48
+ # Creates a notification for each
49
+ # record of the +relation+ so that
50
+ # a many to many relationship can
51
+ # notify all associated objects.
52
+ def create_many!(attributes, relation, options)
53
+ relation.each do |record|
54
+ unless options[:avoid_duplicates] && by_parents(record, attributes[:publisher]).exists?
55
+ create!(attributes.merge(subscriber: record))
56
+ end
57
+ end
58
+ end
59
+
60
+ # create_single!
61
+ #
62
+ # Creates a single notification for the
63
+ # passed entity.
64
+ def create_single!(attributes, relation, options)
65
+ attributes[:subscriber] = relation
66
+ create!(attributes) unless options[:avoid_duplicates] && by_parents(relation, attributes[:publisher]).exists?
67
+ end
68
+ end
69
+
70
+ private_class_method :create_many!, :create_single!
71
+
72
+ def as_json(*)
73
+ Rails.cache.fetch("correspondent_notification_#{id}") do
74
+ attributes.except("updated_at", "subscriber_type", "subscriber_id")
75
+ end
76
+ end
77
+
78
+ def dismiss!
79
+ delete_cache_entry
80
+ update!(dismissed: true)
81
+ end
82
+
83
+ private
84
+
85
+ def delete_cache_entry
86
+ Rails.cache.delete("correspondent_notification_#{id}")
87
+ end
88
+ end
89
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ Correspondent::Engine.routes.draw do
4
+ scope path: ":subscriber_type/:subscriber_id" do
5
+ resources :notifications, only: %i[index destroy] do
6
+ collection do
7
+ get :preview
8
+ end
9
+
10
+ member do
11
+ put :dismiss
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Correspondent
4
+ class Engine < ::Rails::Engine # :nodoc:
5
+ isolate_namespace Correspondent
6
+ config.generators.api_only = true
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Correspondent
4
+ VERSION = "1.0.1"
5
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "correspondent/engine"
4
+ require "async"
5
+
6
+ module Correspondent # :nodoc:
7
+ LAMBDA_PROC_REGEX = /(->.*})|(->.*end)|(proc.*})|(proc.*end)|(Proc\.new.*})|(Proc\.new.*end)/.freeze
8
+
9
+ class << self
10
+ attr_writer :patched_methods
11
+
12
+ # patched_methods
13
+ #
14
+ # Hash with information about methods
15
+ # that need to be patched.
16
+ def patched_methods
17
+ @patched_methods ||= {}.with_indifferent_access
18
+ end
19
+
20
+ # trigger_email
21
+ #
22
+ # Calls the method of a given mailer using the
23
+ # trigger. Triggering only happens if a mailer
24
+ # has been passed as an option.
25
+ #
26
+ # Will invoke methods in this manner:
27
+ #
28
+ # MyMailer.send("make_purchase_email", #<Purchase id: 1...>)
29
+ def trigger_email(data)
30
+ data.dig(:options, :mailer).send("#{data[:trigger]}_email", data[:instance]).deliver_now
31
+ end
32
+
33
+ # <<
34
+ #
35
+ # Adds the notification creation and email sending
36
+ # as asynchronous tasks.
37
+ def <<(data)
38
+ Async do
39
+ unless data.dig(:options, :email_only)
40
+ Correspondent::Notification.create_for!(data.except(:options), data[:options])
41
+ end
42
+
43
+ trigger_email(data) if data.dig(:options, :mailer)
44
+ end
45
+ end
46
+
47
+ # should_notify?
48
+ #
49
+ # Evaluates the if and unless options within
50
+ # the context of a model instance.
51
+ def should_notify?(context, opt)
52
+ if opt[:if].present?
53
+ evaluate_conditional(context, opt[:if])
54
+ elsif opt[:unless].present?
55
+ !evaluate_conditional(context, opt[:unless])
56
+ end
57
+ end
58
+
59
+ # evaluate_conditional
60
+ #
61
+ # Evaluates if or unless regardless of
62
+ # whether it is a proc or a symbol.
63
+ def evaluate_conditional(context, if_or_unless)
64
+ if if_or_unless.is_a?(Proc)
65
+ context.instance_exec(&if_or_unless)
66
+ else
67
+ context.method(if_or_unless).call
68
+ end
69
+ end
70
+ end
71
+
72
+ # notifies
73
+ #
74
+ # Save trigger info and options into the patched_methods
75
+ # hash.
76
+ def notifies(entity, triggers, options = {})
77
+ triggers = Array(triggers)
78
+
79
+ triggers.each do |trigger|
80
+ Correspondent.patched_methods[trigger] ||= []
81
+ Correspondent.patched_methods[trigger] << { entity: entity, options: options }
82
+ end
83
+ end
84
+
85
+ # method_added
86
+ #
87
+ # Callback to patch methods once they are defined.
88
+ # 1. Create an alias of the original method
89
+ # 2. Override method by calling the original
90
+ # 3. Add Correspondent calls for notifications
91
+ def method_added(name)
92
+ patch_info = Correspondent.patched_methods.delete(name)
93
+ return unless patch_info
94
+
95
+ class_eval(<<~PATCH, __FILE__, __LINE__ + 1)
96
+ alias_method :original_#{name}, :#{name}
97
+
98
+ def #{name}(*args, &block)
99
+ result = original_#{name}(*args, &block)
100
+ #{build_async_calls(patch_info, name)}
101
+ result
102
+ end
103
+ PATCH
104
+
105
+ super
106
+ end
107
+
108
+ # ActiveRecord on load hook
109
+ #
110
+ # Extend the module after load so that
111
+ # model class methods are available.
112
+ ActiveSupport.on_load(:active_record) do
113
+ extend Correspondent
114
+ end
115
+
116
+ private
117
+
118
+ # build_async_calls
119
+ #
120
+ # Builds all async call strings needed
121
+ # to patch the method.
122
+ def build_async_calls(patch_info, name) # rubocop:disable Metrics/AbcSize
123
+ patch_info.map do |info|
124
+ info[:options][:unless] = stringify_lambda(info[:options][:unless]) if info[:options][:unless].is_a?(Proc)
125
+ info[:options][:if] = stringify_lambda(info[:options][:if]) if info[:options][:if].is_a?(Proc)
126
+
127
+ async_call_string(info, name)
128
+ end.join("\n")
129
+ end
130
+
131
+ # async_call_string
132
+ #
133
+ # Builds the string for an Async call
134
+ # to send data to Correspondent callbacks.
135
+ def async_call_string(info, name)
136
+ <<~ASYNC_CALL
137
+ if Correspondent.should_notify?(self, #{info[:options].to_s.delete('"')})
138
+ Async do
139
+ Correspondent << {
140
+ instance: self,
141
+ entity: :#{info[:entity]},
142
+ trigger: :#{name},
143
+ options: #{info[:options].to_s.delete('"')}
144
+ }
145
+ end
146
+ end
147
+ ASYNC_CALL
148
+ end
149
+
150
+ # stringify_lambda
151
+ #
152
+ # Transform lambda into a string to be used
153
+ # in method patching.
154
+ def stringify_lambda(lambda)
155
+ lambda.source.scan(LAMBDA_PROC_REGEX).flatten.compact.first
156
+ end
157
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/migration"
4
+
5
+ module Correspondent
6
+ module Generators
7
+ # InstallGenerator
8
+ #
9
+ # Creates the necessary migrations to be able to
10
+ # use the engine.
11
+ class InstallGenerator < ::Rails::Generators::Base
12
+ include Rails::Generators::Migration
13
+
14
+ source_root File.expand_path("templates", __dir__)
15
+ desc "Create Correspondent migrations"
16
+
17
+ def self.next_migration_number(_path)
18
+ if @prev_migration_nr
19
+ @prev_migration_nr += 1
20
+ else
21
+ @prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
22
+ end
23
+
24
+ @prev_migration_nr.to_s
25
+ end
26
+
27
+ def copy_migrations
28
+ migration_template "create_correspondent_notifications.rb",
29
+ "db/migrate/create_correspondent_notifications.rb",
30
+ migration_version: migration_version
31
+
32
+ say "\n"
33
+ say <<~POST_INSTALL_MESSAGE
34
+ Make sure to edit the generated migration and adapt the notifications
35
+ attributes according to the application's need. The only attributes
36
+ that must be kept are the one listed below and the indices.
37
+
38
+ Any other desired attributes can be added and then referenced in the
39
+ `to_notification` method.
40
+
41
+ publisher_type
42
+ publisher_id
43
+ subscriber_type
44
+ subscriber_id
45
+ POST_INSTALL_MESSAGE
46
+ end
47
+
48
+ def migration_version
49
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,19 @@
1
+ class CreateCorrespondentNotifications < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :correspondent_notifications do |t|
4
+ t.string :title
5
+ t.string :content
6
+ t.string :image_url
7
+ t.string :link_url
8
+ t.string :referrer_url
9
+ t.boolean :dismissed, default: false
10
+ t.string :publisher_type, null: false
11
+ t.integer :publisher_id, null: false
12
+ t.string :subscriber_type, null: false
13
+ t.integer :subscriber_id, null: false
14
+ t.index [:publisher_type, :publisher_id], name: "index_correspondent_on_publisher"
15
+ t.index [:subscriber_type, :subscriber_id], name: "index_correspondent_on_subscriber"
16
+ t.timestamps
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # desc "Explaining what the task does"
4
+ # task :correspondent do
5
+ # # Task goes here
6
+ # end
metadata ADDED
@@ -0,0 +1,226 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: correspondent
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Vinicius Stock
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-09-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: async
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: benchmark-ips
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: brakeman
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: purdytest
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rails_best_practices
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop-minitest
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rubocop-performance
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rubocop-rails
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ description: Dead simple configurable user notifications with little overhead.
182
+ email:
183
+ - vinicius.stock@outlook.com
184
+ executables: []
185
+ extensions: []
186
+ extra_rdoc_files: []
187
+ files:
188
+ - MIT-LICENSE
189
+ - README.md
190
+ - Rakefile
191
+ - app/controllers/correspondent/application_controller.rb
192
+ - app/controllers/correspondent/notifications_controller.rb
193
+ - app/jobs/correspondent/application_job.rb
194
+ - app/models/correspondent/application_record.rb
195
+ - app/models/correspondent/notification.rb
196
+ - config/routes.rb
197
+ - lib/correspondent.rb
198
+ - lib/correspondent/engine.rb
199
+ - lib/correspondent/version.rb
200
+ - lib/generators/correspondent/install/install_generator.rb
201
+ - lib/generators/correspondent/install/templates/create_correspondent_notifications.rb
202
+ - lib/tasks/correspondent_tasks.rake
203
+ homepage: https://github.com/vinistock/correspondent
204
+ licenses:
205
+ - MIT
206
+ metadata: {}
207
+ post_install_message:
208
+ rdoc_options: []
209
+ require_paths:
210
+ - lib
211
+ required_ruby_version: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: 2.4.0
216
+ required_rubygems_version: !ruby/object:Gem::Requirement
217
+ requirements:
218
+ - - ">="
219
+ - !ruby/object:Gem::Version
220
+ version: '0'
221
+ requirements: []
222
+ rubygems_version: 3.2.27
223
+ signing_key:
224
+ specification_version: 4
225
+ summary: Dead simple configurable user notifications with little overhead.
226
+ test_files: []