alarmable 1.4.0 → 1.5.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 488b705d3ad253dd224c5918b1e3d4cc9485883e03a58484c20284c195e846b4
4
- data.tar.gz: f6e9a9187839493404543e12cb16a5108d7a602cd89365e0b543559e24e93c76
3
+ metadata.gz: 6cd1edba87ef48cbf7a7f39b8487c816063efd8a9dabf022b7bb451376579856
4
+ data.tar.gz: '068bd2683ad21466a56cca9021014926e97d4a8b89532a70b3334e3b3f06cc31'
5
5
  SHA512:
6
- metadata.gz: c3415ce0216a968361d899b1d5c348430915f6e3cea437bf919958ed969a92566a9159314afcbf556c3ef9df0deb94538ed84db6f1063b9732c6a496735aed7b
7
- data.tar.gz: 8a1383152dca9d66814529f676d205511f3d3bcd3c8b3975303bf2781a9be240d3695ef64e11541e3cd00f674dfa14789e699c17519fab968fe335b7608b7dfb
6
+ metadata.gz: 06ffb936a32be6107458c5a6754c87829c897327a97c7563f67e5602d23a4407d108088cb0235265871230fc715a4091671a2ef7f04e19452cb7c3b9537a50a0
7
+ data.tar.gz: 03440c6dd7606f5c7f80d7dc320ad87d19ab3138fde9946d4c1209d8976ec140da40bb5f4e5de8ed61eca062ea118d37c66136353290d83d09a7901bbd70b563
data/CHANGELOG.md CHANGED
@@ -2,9 +2,13 @@
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
+
5
9
  ### 1.4.0 (12 January 2025)
6
10
 
7
- * TODO: Replace this bullet point with an actual description of a change.
11
+ * Just a retag of 1.3.0
8
12
 
9
13
  ### 1.3.0 (3 January 2025)
10
14
 
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.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.4.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-12 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