alarmable 1.4.0 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 488b705d3ad253dd224c5918b1e3d4cc9485883e03a58484c20284c195e846b4
4
- data.tar.gz: f6e9a9187839493404543e12cb16a5108d7a602cd89365e0b543559e24e93c76
3
+ metadata.gz: 577dcfa9cb1b37ba1ee9a4025ce045d19f36ae61d371270049310e696c5ee32c
4
+ data.tar.gz: 38bccc14a3f155e50d1397a23a3582b7546c0995785b47da3dd8598e83743c4c
5
5
  SHA512:
6
- metadata.gz: c3415ce0216a968361d899b1d5c348430915f6e3cea437bf919958ed969a92566a9159314afcbf556c3ef9df0deb94538ed84db6f1063b9732c6a496735aed7b
7
- data.tar.gz: 8a1383152dca9d66814529f676d205511f3d3bcd3c8b3975303bf2781a9be240d3695ef64e11541e3cd00f674dfa14789e699c17519fab968fe335b7608b7dfb
6
+ metadata.gz: '087f42cdcbe1e7fcd64b88ef7f8c7ec60607296cc071861974692b3cafbefbfd22943d4432962a6e78dfb7d170d06ae0fae5e32f23d2b4bb082c14b75b1b6049'
7
+ data.tar.gz: 36c38cacac483c1a242e2f21fd033d0bdb6e226fbcd452d4596d3aae55c7550340c11cfb7ba643daf43699796f041f6a4bacd0a8c4f5bc954537ea8dbfb0069c
data/CHANGELOG.md CHANGED
@@ -2,9 +2,17 @@
2
2
 
3
3
  * TODO: Replace this bullet point with an actual description of a change.
4
4
 
5
+ ### 1.5.1 (17 January 2025)
6
+
7
+ * Added the logger dependency (#15)
8
+
9
+ ### 1.5.0 (14 January 2025)
10
+
11
+ * Switched to Zeitwerk as autoloader (#14)
12
+
5
13
  ### 1.4.0 (12 January 2025)
6
14
 
7
- * TODO: Replace this bullet point with an actual description of a change.
15
+ * Just a retag of 1.3.0
8
16
 
9
17
  ### 1.3.0 (3 January 2025)
10
18
 
data/alarmable.gemspec CHANGED
@@ -41,4 +41,5 @@ Gem::Specification.new do |spec|
41
41
  spec.add_dependency 'activerecord', '>= 6.1'
42
42
  spec.add_dependency 'activesupport', '>= 6.1'
43
43
  spec.add_dependency 'hashdiff', '~> 1.0'
44
+ spec.add_dependency 'zeitwerk', '~> 2.6'
44
45
  end
@@ -3,7 +3,7 @@
3
3
  # The gem version details.
4
4
  module Alarmable
5
5
  # The version of the +alarmable+ gem
6
- VERSION = '1.4.0'
6
+ VERSION = '1.5.1'
7
7
 
8
8
  class << self
9
9
  # Returns the version of gem as a string.
data/lib/alarmable.rb CHANGED
@@ -1,10 +1,222 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'zeitwerk'
4
+ require 'logger'
3
5
  require 'active_support'
4
6
  require 'active_record'
5
7
  require 'active_job'
6
8
  require 'active_job/cancel'
7
9
  require 'hashdiff'
8
10
 
9
- require 'alarmable/version'
10
- require 'alarmable/concern'
11
+ # A reusable alarm extension to Active Record models. It adds support for the
12
+ # maintenance of Active Job's (create, update (cancel)) which are schedules
13
+ # for the given alarms. We check for changes on the alarms hash and perform
14
+ # updates accordingly.
15
+ #
16
+ # This concern requires the persistence (and availability) of two properties.
17
+ #
18
+ # * The first is the JSONB array which holds the alarms. (+alarms+)
19
+ # * The seconds is the JSONB array which holds the ids of
20
+ # scheduled alarm jobs. (+alarm_jobs+)
21
+ #
22
+ # rails generate migration AddAlarmsAndAlarmJobsToEntity \
23
+ # alarms:jsonb alarm_jobs:jsonb
24
+ #
25
+ # Furthermore a Active Record model which uses this concern must define the
26
+ # Active Job class which will be scheduled. (+alarm_job+) The user must also
27
+ # define the base date property of the owning side.
28
+ # (+alarm_base_date_property+) This base date is mandatory to calculate the
29
+ # correct alarm date/time. When the base date is not set (+nil+) no new
30
+ # notification job will be enqueued. When the base date is unset on an
31
+ # update, the previously enqueued job will be canceled.
32
+ #
33
+ # The alarms hash needs to be an array in the following format:
34
+ #
35
+ # [
36
+ # {
37
+ # "channel": "email", # email, push, web_notification, etc..
38
+ # "before_minutes": 15 # start_at - before_minutes, >= 1
39
+ # }
40
+ # ]
41
+ #
42
+ # The given alarm job class will be scheduled with the following two
43
+ # arguments.
44
+ #
45
+ # * id - The class/instance id of the record which owns the alarm
46
+ # * alarm - The alarm hash itself (see the format above)
47
+ #
48
+ # A suitable alarm job perform method should look like this:
49
+ #
50
+ # # @param id [String] The entity id
51
+ # # @param alarm [Hash] The alarm object
52
+ # def perform(id, alarm)
53
+ # # Do something special for +alarm.channel+ ..
54
+ # end
55
+ module Alarmable
56
+ # Setup a Zeitwerk autoloader instance and configure it
57
+ loader = Zeitwerk::Loader.for_gem
58
+
59
+ # Finish the auto loader configuration
60
+ loader.setup
61
+
62
+ # Make sure to eager load all constants
63
+ loader.eager_load
64
+
65
+ extend ActiveSupport::Concern
66
+
67
+ class_methods do
68
+ # Getter/Setter
69
+ #
70
+ # :reek:Attribute because thats what this thing is about
71
+ attr_accessor :alarm_job, :alarm_base_date_property
72
+ end
73
+
74
+ # rubocop:disable Metrics/BlockLength because Active Support like it
75
+ included do
76
+ # Hooks
77
+ after_initialize :validate_alarm_settings, :alarm_defaults
78
+
79
+ # Here comes a little cheat sheet when and what action is performed
80
+ # on the alarm jobs.
81
+ #
82
+ # create | [ time check, reschedule]
83
+ # update | dirty check, [cancel job, time check, reschedule]
84
+ # destroy | [cancel job ]
85
+ after_create :reschedule_alarm_jobs
86
+ before_update :alarms_update_callback
87
+ before_destroy :alarms_destroy_callback
88
+
89
+ # Getter for the alarm job class.
90
+ #
91
+ # @return [Class] The alarm job class
92
+ def alarm_job
93
+ self.class.alarm_job
94
+ end
95
+
96
+ # Getter for the alarm base date property.
97
+ #
98
+ # @return [Symbol] The user defined base date property
99
+ def alarm_base_date_property
100
+ self.class.alarm_base_date_property
101
+ end
102
+
103
+ # Set some defaults on the relevant alarm properties.
104
+ def alarm_defaults
105
+ self.alarms ||= []
106
+ self.alarm_jobs ||= {}
107
+ end
108
+
109
+ # Validate the presence of the +alarm_job+ property and the accessibility
110
+ # of the specified class. Also validate the +alarm_base_date_property+
111
+ # setting.
112
+ #
113
+ # rubocop:disable Style/GuardClause because its fine like this
114
+ # :reek:NilCheck because we validate concern usage
115
+ def validate_alarm_settings
116
+ raise 'Alarmable +alarm_job+ is not configured' if alarm_job.nil?
117
+ unless alarm_job.is_a? Class
118
+ raise 'Alarmable +alarm_job+ is not instantiable'
119
+ end
120
+ if alarm_base_date_property.nil?
121
+ raise 'Alarmable +alarm_base_date_property+ is not configured'
122
+ end
123
+ unless has_attribute? alarm_base_date_property
124
+ raise 'Alarmable +alarm_base_date_property+ is not usable'
125
+ end
126
+ end
127
+ # rubocop:enable Style/GuardClause
128
+
129
+ # Generate a unique and recalculatable identifier for a given alarm
130
+ # object. We build a hash of the primary keys (before_minutes and
131
+ # channel) to achive this. Afterwards, this alarm id is used to
132
+ # reference dedicated scheduled jobs and track their updates. (Or cancel
133
+ # them accordingly)
134
+ #
135
+ # @param channel [String] The alarm channel
136
+ # @param before_minutes [Integer] The minutes before the alarm starts
137
+ # @return [String] The unique alarm id
138
+ #
139
+ # :reek:UtilityFunction because its a utility, for sure
140
+ def alarm_id(channel, before_minutes)
141
+ (Digest::MD5.new << "#{channel}#{before_minutes}").to_s
142
+ end
143
+
144
+ # Schedule a new Active Job for the alarm notification. This method takes
145
+ # care of the notification time (+date) and will not touch anything when
146
+ # the desired time already passed. It cancels the correct job for the
147
+ # given combination, when it is present. In the end it schedules a new
148
+ # (renewed) job for the given alarm settings.
149
+ #
150
+ # @param alarm [Hash] The alarm object
151
+ # @return [Object] The new alarm_jobs instance (partial)
152
+ # Example: { "alarm id": "job id" }
153
+ #
154
+ # rubocop:disable Metrics/AbcSize because its already broken down
155
+ # :reek:TooManyStatements because see above
156
+ # :reek:NilCheck because we dont want to cancel 'nil' job id
157
+ # :reek:DuplicateMethodCall because hash access is fast
158
+ def reschedule_alarm_job(alarm)
159
+ # Symbolize the hash keys (just to be sure).
160
+ alarm = alarm.symbolize_keys
161
+
162
+ # Calculate the alarm id for job canceling and cancel a found job.
163
+ id = alarm_id(alarm[:channel], alarm[:before_minutes])
164
+ previous_job_id = alarm_jobs.try(:[], id)
165
+ alarm_job.cancel(previous_job_id) unless previous_job_id.nil?
166
+
167
+ base_date = self[alarm_base_date_property]
168
+
169
+ # When the base date is not set, we schedule not a new notification job.
170
+ return {} if base_date.nil?
171
+
172
+ # Calculate the time when the job should run.
173
+ notify_at = base_date - alarm[:before_minutes].minutes
174
+
175
+ # Do nothing when the notification date already passed.
176
+ return {} if Time.current >= notify_at
177
+
178
+ # Put a new job to the queue with the new (current) job execution date.
179
+ job = alarm_job.set(wait_until: notify_at).perform_later(self.id, alarm)
180
+
181
+ # Construct a new alarm_jobs partial instance for this job
182
+ { id => job.job_id }
183
+ end
184
+ # rubocop:enable Metrics/AbcSize
185
+
186
+ # Initiate a reschedule for each alarm in the alarm settings and
187
+ # cancel all left-overs.
188
+ #
189
+ # :reek:TooManyStatements because its already broken down
190
+ def reschedule_alarm_jobs
191
+ # Perform the reschedule of all the current alarms.
192
+ new_alarm_jobs = alarms.each_with_object({}) do |alarm, memo|
193
+ memo.merge!(reschedule_alarm_job(alarm))
194
+ end
195
+
196
+ # Detect the differences from the original alarm_jobs hash to the new
197
+ # built (by partials) alarm_jobs hash. The jobs from negative
198
+ # differences must be canceled.
199
+ diff = Hashdiff.diff(alarm_jobs, new_alarm_jobs)
200
+
201
+ diff.select { |prop| prop.first == '-' }.each do |prop|
202
+ alarm_job.cancel(prop.last)
203
+ end
204
+
205
+ # Update the alarm_jobs reference pool with our fresh hash. Bypass the
206
+ # regular validation and callbacks here, this is required to not stuck
207
+ # in endless create-update loops.
208
+ update_columns(alarm_jobs: new_alarm_jobs)
209
+ end
210
+
211
+ # Reschedule only on updates when the alarm settings are changed.
212
+ def alarms_update_callback
213
+ reschedule_alarm_jobs if alarms_changed?
214
+ end
215
+
216
+ # Cancel all alarm notification jobs on parent destroy.
217
+ def alarms_destroy_callback
218
+ alarm_jobs.each_value { |job_id| alarm_job.cancel(job_id) }
219
+ end
220
+ end
221
+ # rubocop:enable Metrics/BlockLength
222
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alarmable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hermann Mayer
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-01-12 00:00:00.000000000 Z
11
+ date: 2025-01-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '1.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: zeitwerk
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.6'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.6'
83
97
  description: This is a reusable alarm concern for Active Recordmodels. It adds support
84
98
  for the automatic maintenanceof Active Job's which are scheduled for the givenalarms.
85
99
  email:
@@ -118,7 +132,6 @@ files:
118
132
  - gemfiles/rails_6.1.gemfile
119
133
  - gemfiles/rails_7.1.gemfile
120
134
  - lib/alarmable.rb
121
- - lib/alarmable/concern.rb
122
135
  - lib/alarmable/version.rb
123
136
  homepage:
124
137
  licenses:
@@ -1,203 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # A reusable alarm extension to Active Record models. It adds support for the
4
- # maintenance of Active Job's (create, update (cancel)) which are schedules for
5
- # the given alarms. We check for changes on the alarms hash and perform
6
- # updates accordingly.
7
- #
8
- # This concern requires the persistence (and availability) of two properties.
9
- #
10
- # * The first is the JSONB array which holds the alarms. (+alarms+)
11
- # * The seconds is the JSONB array which holds the ids of
12
- # scheduled alarm jobs. (+alarm_jobs+)
13
- #
14
- # rails generate migration AddAlarmsAndAlarmJobsToEntity \
15
- # alarms:jsonb alarm_jobs:jsonb
16
- #
17
- # Furthermore a Active Record model which uses this concern must define the
18
- # Active Job class which will be scheduled. (+alarm_job+) The user must also
19
- # define the base date property of the owning side.
20
- # (+alarm_base_date_property+) This base date is mandatory to calculate the
21
- # correct alarm date/time. When the base date is not set (+nil+) no new
22
- # notification job will be enqueued. When the base date is unset on an update,
23
- # the previously enqueued job will be canceled.
24
- #
25
- # The alarms hash needs to be an array in the following format:
26
- #
27
- # [
28
- # {
29
- # "channel": "email", # email, push, web_notification, etc..
30
- # "before_minutes": 15 # start_at - before_minutes, >= 1
31
- # }
32
- # ]
33
- #
34
- # The given alarm job class will be scheduled with the following two arguments.
35
- #
36
- # * id - The class/instance id of the record which owns the alarm
37
- # * alarm - The alarm hash itself (see the format above)
38
- #
39
- # A suitable alarm job perform method should look like this:
40
- #
41
- # # @param id [String] The entity id
42
- # # @param alarm [Hash] The alarm object
43
- # def perform(id, alarm)
44
- # # Do something special for +alarm.channel+ ..
45
- # end
46
- module Alarmable
47
- extend ActiveSupport::Concern
48
-
49
- class_methods do
50
- # Getter/Setter
51
- #
52
- # :reek:Attribute because thats what this thing is about
53
- attr_accessor :alarm_job, :alarm_base_date_property
54
- end
55
-
56
- # rubocop:disable Metrics/BlockLength because Active Support like it
57
- included do
58
- # Hooks
59
- after_initialize :validate_alarm_settings, :alarm_defaults
60
-
61
- # Here comes a little cheat sheet when and what action is performed
62
- # on the alarm jobs.
63
- #
64
- # create | [ time check, reschedule]
65
- # update | dirty check, [cancel job, time check, reschedule]
66
- # destroy | [cancel job ]
67
- after_create :reschedule_alarm_jobs
68
- before_update :alarms_update_callback
69
- before_destroy :alarms_destroy_callback
70
-
71
- # Getter for the alarm job class.
72
- #
73
- # @return [Class] The alarm job class
74
- def alarm_job
75
- self.class.alarm_job
76
- end
77
-
78
- # Getter for the alarm base date property.
79
- #
80
- # @return [Symbol] The user defined base date property
81
- def alarm_base_date_property
82
- self.class.alarm_base_date_property
83
- end
84
-
85
- # Set some defaults on the relevant alarm properties.
86
- def alarm_defaults
87
- self.alarms ||= []
88
- self.alarm_jobs ||= {}
89
- end
90
-
91
- # Validate the presence of the +alarm_job+ property and the accessibility
92
- # of the specified class. Also validate the +alarm_base_date_property+
93
- # setting.
94
- #
95
- # rubocop:disable Style/GuardClause because its fine like this
96
- # :reek:NilCheck because we validate concern usage
97
- def validate_alarm_settings
98
- raise 'Alarmable +alarm_job+ is not configured' if alarm_job.nil?
99
- unless alarm_job.is_a? Class
100
- raise 'Alarmable +alarm_job+ is not instantiable'
101
- end
102
- if alarm_base_date_property.nil?
103
- raise 'Alarmable +alarm_base_date_property+ is not configured'
104
- end
105
- unless has_attribute? alarm_base_date_property
106
- raise 'Alarmable +alarm_base_date_property+ is not usable'
107
- end
108
- end
109
- # rubocop:enable Style/GuardClause
110
-
111
- # Generate a unique and recalculatable identifier for a given alarm object.
112
- # We build a hash of the primary keys (before_minutes and channel) to
113
- # achive this. Afterwards, this alarm id is used to reference dedicated
114
- # scheduled jobs and track their updates. (Or cancel them accordingly)
115
- #
116
- # @param channel [String] The alarm channel
117
- # @param before_minutes [Integer] The minutes before the alarm starts
118
- # @return [String] The unique alarm id
119
- #
120
- # :reek:UtilityFunction because its a utility, for sure
121
- def alarm_id(channel, before_minutes)
122
- (Digest::MD5.new << "#{channel}#{before_minutes}").to_s
123
- end
124
-
125
- # Schedule a new Active Job for the alarm notification. This method takes
126
- # care of the notification time (+date) and will not touch anything when
127
- # the desired time already passed. It cancels the correct job for the
128
- # given combination, when it is present. In the end it schedules a new
129
- # (renewed) job for the given alarm settings.
130
- #
131
- # @param alarm [Hash] The alarm object
132
- # @return [Object] The new alarm_jobs instance (partial)
133
- # Example: { "alarm id": "job id" }
134
- #
135
- # rubocop:disable Metrics/AbcSize because its already broken down
136
- # :reek:TooManyStatements because see above
137
- # :reek:NilCheck because we dont want to cancel 'nil' job id
138
- # :reek:DuplicateMethodCall because hash access is fast
139
- def reschedule_alarm_job(alarm)
140
- # Symbolize the hash keys (just to be sure).
141
- alarm = alarm.symbolize_keys
142
-
143
- # Calculate the alarm id for job canceling and cancel a found job.
144
- id = alarm_id(alarm[:channel], alarm[:before_minutes])
145
- previous_job_id = alarm_jobs.try(:[], id)
146
- alarm_job.cancel(previous_job_id) unless previous_job_id.nil?
147
-
148
- base_date = self[alarm_base_date_property]
149
-
150
- # When the base date is not set, we schedule not a new notification job.
151
- return {} if base_date.nil?
152
-
153
- # Calculate the time when the job should run.
154
- notify_at = base_date - alarm[:before_minutes].minutes
155
-
156
- # Do nothing when the notification date already passed.
157
- return {} if Time.current >= notify_at
158
-
159
- # Put a new job to the queue with the new (current) job execution date.
160
- job = alarm_job.set(wait_until: notify_at).perform_later(self.id, alarm)
161
-
162
- # Construct a new alarm_jobs partial instance for this job
163
- { id => job.job_id }
164
- end
165
- # rubocop:enable Metrics/AbcSize
166
-
167
- # Initiate a reschedule for each alarm in the alarm settings and
168
- # cancel all left-overs.
169
- #
170
- # :reek:TooManyStatements because its already broken down
171
- def reschedule_alarm_jobs
172
- # Perform the reschedule of all the current alarms.
173
- new_alarm_jobs = alarms.each_with_object({}) do |alarm, memo|
174
- memo.merge!(reschedule_alarm_job(alarm))
175
- end
176
-
177
- # Detect the differences from the original alarm_jobs hash to the new
178
- # built (by partials) alarm_jobs hash. The jobs from negative differences
179
- # must be canceled.
180
- diff = Hashdiff.diff(alarm_jobs, new_alarm_jobs)
181
-
182
- diff.select { |prop| prop.first == '-' }.each do |prop|
183
- alarm_job.cancel(prop.last)
184
- end
185
-
186
- # Update the alarm_jobs reference pool with our fresh hash. Bypass the
187
- # regular validation and callbacks here, this is required to not stuck in
188
- # endless create-update loops.
189
- update_columns(alarm_jobs: new_alarm_jobs)
190
- end
191
-
192
- # Reschedule only on updates when the alarm settings are changed.
193
- def alarms_update_callback
194
- reschedule_alarm_jobs if alarms_changed?
195
- end
196
-
197
- # Cancel all alarm notification jobs on parent destroy.
198
- def alarms_destroy_callback
199
- alarm_jobs.each_value { |job_id| alarm_job.cancel(job_id) }
200
- end
201
- end
202
- # rubocop:enable Metrics/BlockLength
203
- end