alarmable 1.3.0 → 1.5.0

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