correspondent 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []