auto_alert 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6115b3cc9d444af3c92d33b3ad3c3152040e0912254a36959d46e732dd3e3634
4
+ data.tar.gz: 2da6fae49f957aeb5be28a00dd9921feb699ad8d63ea7b83d906e073e9ba6079
5
+ SHA512:
6
+ metadata.gz: 7934f6257a750d9fa24814e3789e2c30f81c84d903d55688c8fd38a11d6c3ee11f46645cef1101fe43f29dbea8227c19538baff07987d5ae6bda92e7105fc8f7
7
+ data.tar.gz: dbbb8a284baf98324de67846bc216ec48ef9912f74e09b1ef9e5120849cb6970bdf8dbe0f1bc61272532abbec2d672022db01e22ad5eec5800d8dcdee1e5d738
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2021 Brendan Tang
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,175 @@
1
+ # auto_alert
2
+
3
+
4
+ [`auto_alert`](https://github.com/brendantang/auto_alert) provides a simple DSL to declare conditions that should raise alerts on your ActiveRecord models.
5
+ Alerts are saved in a database table with a [polymorphic association](https://guides.rubyonrails.org/association_basics.html#polymorphic-associations) to the model which triggered their creation.
6
+ It's possible to define different conditions to raise, resolve, and re-raise an alert.
7
+
8
+
9
+
10
+ ## Quick example
11
+
12
+
13
+ Imagine a to-do list application where tasks with a due date in the past should be flagged with an alert.
14
+ With `auto_alert`, you can modify your Task model like so:
15
+
16
+
17
+ ```ruby
18
+ class Task < ApplicationRecord
19
+ acts_as_alertable
20
+
21
+ raises_alert :past_due,
22
+ on: :past_due_date?,
23
+ message: "was due"
24
+
25
+ private
26
+ def past_due_date?
27
+ due_date < Date.current and !done
28
+ end
29
+ ```
30
+
31
+
32
+ Then you can call the `scan_for_alerts!` on any task and, if `past_due_date?` returns `true`, an alert record belonging to that task will be created.
33
+ Instances of `Task` will also have a `past_due_alert` getter method to fetch their associated `past_due` alert, if any.
34
+
35
+
36
+
37
+ ## Installation
38
+
39
+
40
+ ### Install `auto_alert`
41
+
42
+ Add this line to your application's Gemfile:
43
+
44
+ ```ruby
45
+ gem 'auto_alert'
46
+ ```
47
+
48
+ And then execute:
49
+ ```bash
50
+ $ bundle
51
+ ```
52
+
53
+ ### Create `Alert` model
54
+
55
+ `auto_alert` assumes there is an `Alert` model to store your alerts in the database.
56
+
57
+ An appropriate model can be built using the Rails generator by running something like, `rails generate model Alert alertable:references resolved:boolean kind:string message:string`.
58
+ Then edit the resulting migration so that it looks like [this example](test/dummy/db/migrate/20210613222049_create_alerts.rb).
59
+
60
+ Make sure to edit the alert model file to register it with `auto_alert`:
61
+
62
+ ```ruby
63
+ # app/models/alert.rb
64
+ class Alert < ApplicationRecord
65
+ acts_as_alert # add this line
66
+ end
67
+ ```
68
+
69
+ (In the future I'd like to add an `install` rake task to automate this process.)
70
+
71
+ `auto_alert` assumes the alert relation uses the table called `alerts`, but you can specify a different relation:
72
+
73
+ ```ruby
74
+ # app/models/task.rb
75
+ class Task < ApplicationRecord
76
+ acts_as_alertable # will default to using the Alert model
77
+ end
78
+
79
+ # app/models/task_list.rb
80
+ class TaskList < ApplicationRecord
81
+ acts_as_alertable with_table: :special_alerts # will look for a SpecialAlert model
82
+ end
83
+ ```
84
+
85
+
86
+
87
+ ## Usage
88
+
89
+
90
+ Calling `acts_as_alertable` in your model definition:
91
+ - Allows you to use the `raises_alert` class method to declare the conditions that should raise or resolve an alert
92
+ - Registers that model instances have alerts
93
+ - Uses [polymorphic association](https://guides.rubyonrails.org/association_basics.html#polymorphic-associations), so you can use all the instance methods it provides
94
+ - Also creates convenience getters for each kind of alert that model can raise (i.e. `task.past_due_alert`)
95
+ - Provides the instance method `scan_for_alerts!`, which creates or updates alert records according to the declared `raises_alert` conditions.
96
+
97
+
98
+ ### Scanning for alerts
99
+
100
+ `auto_alert` makes no assumptions about when you'd like to check if alerts should be raised or resolved.
101
+
102
+ For simple use cases, it can make sense to call `scan_for_alerts!` on specific instances from your controller, or to hook into one of the ActiveRecord [callbacks](https://guides.rubyonrails.org/active_record_callbacks.html):
103
+ ```ruby
104
+ class MyModel < ApplicationRecord
105
+ acts_as_alertable
106
+ # ...
107
+ after_save :scan_for_alerts!
108
+ ```
109
+
110
+ For more complicated use cases, especially where alert conditions rely on factors external to the model instance itself, it can make sense to use a [job](https://guides.rubyonrails.org/active_job_basics.html) to asynchronously call `scan_for_alerts!` on big batches of records.
111
+
112
+
113
+ ### Alert records
114
+
115
+ `auto_alert` assumes there is an ActiveRecord model for the table `alerts`, with at least:
116
+ - A polymorphic `alertable` reference (pointing to the record which triggered the alert)
117
+ - A boolean `resolved` attribute (indicating the alert is no longer relevant)
118
+ - A `kind` attribute (each alertable record has only one alert of each kind—`past_due` in the example above)
119
+ - Can be a text column or a Rails enumerable using a numeric column ([example](./test/dummy/app/models/special_alert.rb))
120
+ - A string `message` attribute describing the alert details
121
+
122
+ Instructions for creating such a model are in the Installation secion under [Create Alert model](#create-alert-model).
123
+
124
+
125
+ ### Declaring rules to raise and resolve alerts
126
+
127
+ The `on` parameter to `raises_alert` can take a method name as [above](#quick-example), or a proc:
128
+
129
+ ```ruby
130
+ raises_alert :past_due,
131
+ on: ->(t) { t.due_date < Date.current and !t.done }
132
+ ```
133
+
134
+ You can also pass a `resolve_on` parameter to specify the condition for marking the alert `resolved`.
135
+ A proc or method name is acceptable.
136
+
137
+ ```ruby
138
+ class Order < ApplicationRecord
139
+ acts_as_alertable
140
+
141
+ raises_alert :week_old,
142
+ on: ->(order) { order.placed <= Date.current - 1.week },
143
+ resolve_on: :shipped
144
+ # Now changing the order date won't resolve the alert, but marking it `shipped` will.
145
+ ```
146
+
147
+ If no `resolve_on` parameter is passed, the inverse of the `on` condition is used.
148
+
149
+
150
+ ### Re-raising alerts
151
+
152
+ By default, an alert which is `resolved` will not be re-raised, even if the `on` condition is met again.
153
+ If you pass the `reraise` option `true`, the alert's `resolved` attribute will be updated to `false` every time the `on` condition is met.
154
+
155
+ ```ruby
156
+ class Temperature < ApplicationRecord
157
+ acts_as_alertable
158
+
159
+ raises_alert :too_cold,
160
+ on: ->(temp) { temp.farenheit < 32 },
161
+ reraise: true
162
+ ```
163
+
164
+ You can also give a proc or method name to the `reraise` option to use a condition other than the initial `on` condition.
165
+
166
+
167
+ ### Messages
168
+
169
+ Alert messages can be built using a string, a proc, or a method name.
170
+ When an alert is re-raised, its message will be built again.
171
+
172
+
173
+
174
+ ## License
175
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ require "rake/testtask"
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << 'test'
9
+ t.pattern = 'test/**/*_test.rb'
10
+ t.verbose = false
11
+ end
12
+
13
+ task default: :test
data/lib/auto_alert.rb ADDED
@@ -0,0 +1,9 @@
1
+ require "auto_alert/version"
2
+ require "auto_alert/railtie"
3
+ require "auto_alert/acts_as_alertable"
4
+ require "auto_alert/acts_as_alert"
5
+ require "auto_alert/checker"
6
+
7
+ module AutoAlert
8
+ # Your code goes here...
9
+ end
@@ -0,0 +1,27 @@
1
+ module AutoAlert
2
+ module ActsAsAlert
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ # Class method to register a model as alertable
7
+ def acts_as_alert
8
+ belongs_to :alertable, polymorphic: true
9
+ validates :kind,
10
+ uniqueness: {
11
+ scope: :alertable,
12
+ message: ->(record, data) { "This #{record.alertable_type} already has a '#{data[:value]}' alert." },
13
+ }
14
+ include AutoAlert::ActsAsAlert::LocalInstanceMethods
15
+ extend AutoAlert::ActsAsAlert::SingletonMethods
16
+ end
17
+ end
18
+
19
+ module LocalInstanceMethods
20
+ end
21
+
22
+ module SingletonMethods
23
+ end
24
+ end
25
+ end
26
+
27
+ ActiveRecord::Base.send(:include, AutoAlert::ActsAsAlert)
@@ -0,0 +1,50 @@
1
+ module AutoAlert
2
+ module ActsAsAlertable
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ # Class method to register a model as alertable
7
+ def acts_as_alertable(with_table: :alerts)
8
+ cattr_accessor :alerts_table_name, default: with_table
9
+ alias_attribute :alerts, with_table unless with_table.to_sym == :alerts
10
+ cattr_accessor :alert_checkers, default: []
11
+ has_many with_table.to_sym, as: :alertable, dependent: :destroy
12
+ include AutoAlert::ActsAsAlertable::LocalInstanceMethods
13
+ extend AutoAlert::ActsAsAlertable::SingletonMethods
14
+ end
15
+ end
16
+
17
+ module LocalInstanceMethods
18
+ def unresolved_alerts
19
+ alerts.where(resolved: false)
20
+ end
21
+
22
+ def has_unresolved_alerts?
23
+ unresolved_alerts.size > 0
24
+ end
25
+
26
+ # Check if any alerts should be raised or resolveed
27
+ def scan_for_alerts!
28
+ self.class.alert_checkers.each do |checker|
29
+ checker.check(self)
30
+ end
31
+ end
32
+ end
33
+
34
+ module SingletonMethods
35
+ def raises_alert(kind, on:, resolve_on: nil, message: nil, reraise: false)
36
+ checker = AutoAlert::Checker.new(kind, on, resolve_on, reraise, message)
37
+ alert_checkers << checker
38
+ define_method "#{kind}_alert" do
39
+ alerts.find_by(kind: kind)
40
+ end
41
+ end
42
+
43
+ def alert_kinds
44
+ alert_checkers.map do |checker| checker.kind end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ ActiveRecord::Base.send(:include, AutoAlert::ActsAsAlertable)
@@ -0,0 +1,75 @@
1
+ module AutoAlert
2
+ class Checker
3
+ attr_reader :kind
4
+
5
+ def initialize(alert_kind, raise_condition, resolve_condition, reraise_condition = false, message_builder)
6
+ @kind = alert_kind
7
+ @raise_condition = normalize_check_condition raise_condition
8
+ @resolve_condition =
9
+ resolve_condition ?
10
+ normalize_check_condition(resolve_condition) :
11
+ ->(a) { not @raise_condition.call(a) }
12
+ @reraise_condition = if !reraise_condition # If nil or false
13
+ nil
14
+ elsif reraise_condition == true
15
+ @raise_condition
16
+ else
17
+ normalize_check_condition reraise_condition
18
+ end
19
+ @message_builder = message_builder
20
+ end
21
+
22
+ def check(alertable)
23
+ existing_alert = alertable.send("#{@kind}_alert")
24
+
25
+ case [existing_alert, @reraise_condition, existing_alert&.resolved]
26
+
27
+ # Check for alert for the first time
28
+ in nil, _, _
29
+ if @raise_condition.call(alertable)
30
+ alertable.alerts.create(kind: @kind, message: message(alertable), resolved: false)
31
+ end
32
+
33
+ # Check if alert should be resolveed
34
+ in alert, _, false
35
+ alert.update(resolved: true) if @resolve_condition.call(alertable)
36
+
37
+ # Resolved alert should not be re-raised
38
+ in alert, nil, true
39
+ return
40
+
41
+ # Check if resolved alert should be re-raised
42
+ in alert, reraise_condition, true
43
+ if reraise_condition.call(alertable)
44
+ alert.update(message: message(alertable), resolved: false)
45
+ end
46
+
47
+ else
48
+ return
49
+ end
50
+ end
51
+
52
+ def message(alertable)
53
+ case @message_builder
54
+ in Proc => p
55
+ p.call(alertable)
56
+ in Symbol => s if alertable.respond_to?(s, true)
57
+ alertable.send(@message_builder)
58
+ else
59
+ @message_builder
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def normalize_check_condition(condition)
66
+ if condition.respond_to?(:call)
67
+ condition
68
+ else
69
+ lambda do |record|
70
+ record.send(condition)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,4 @@
1
+ module AutoAlert
2
+ class Railtie < ::Rails::Railtie
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module AutoAlert
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :auto_alert do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: auto_alert
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Brendan Tang
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-06-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 6.1.3
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 6.1.3.2
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: 6.1.3
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 6.1.3.2
33
+ description: A plugin for Rails which makes it easy to specify conditions to raise
34
+ or resolve alert records associated with your ActiveRecord models.
35
+ email:
36
+ - b@brendantang.net
37
+ executables: []
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - MIT-LICENSE
42
+ - README.md
43
+ - Rakefile
44
+ - lib/auto_alert.rb
45
+ - lib/auto_alert/acts_as_alert.rb
46
+ - lib/auto_alert/acts_as_alertable.rb
47
+ - lib/auto_alert/checker.rb
48
+ - lib/auto_alert/railtie.rb
49
+ - lib/auto_alert/version.rb
50
+ - lib/tasks/auto_alert_tasks.rake
51
+ homepage: https://github.com/brendantang/auto_alert
52
+ licenses:
53
+ - MIT
54
+ metadata:
55
+ homepage_uri: https://github.com/brendantang/auto_alert
56
+ source_code_uri: https://github.com/brendantang/auto_alert
57
+ changelog_uri: https://github.com/brendantang/auto_alert
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 3.2.16
74
+ signing_key:
75
+ specification_version: 4
76
+ summary: Automatically raise and dismiss alerts on your ActiveRecord models.
77
+ test_files: []