noticent 0.0.1.pre.pre → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.ruby-version +1 -2
- data/Gemfile.lock +154 -74
- data/LICENSE +201 -0
- data/README.md +81 -14
- data/config.ru +9 -0
- data/lib/noticent.rb +2 -1
- data/lib/noticent/active_record_opt_in_provider.rb +10 -1
- data/lib/noticent/channel.rb +24 -31
- data/lib/noticent/config.rb +77 -18
- data/lib/noticent/definitions/alert.rb +80 -6
- data/lib/noticent/definitions/channel.rb +7 -2
- data/lib/noticent/definitions/scope.rb +10 -6
- data/lib/noticent/dispatcher.rb +16 -10
- data/lib/noticent/version.rb +1 -1
- data/lib/noticent/view.rb +16 -14
- data/noticent.gemspec +27 -21
- data/testing/channels/email.rb +1 -0
- data/testing/channels/exclusive.rb +9 -0
- data/testing/channels/simple.rb +9 -0
- data/testing/payloads/comment_payload.rb +10 -0
- data/testing/payloads/post_payload.rb +44 -0
- data/testing/views/email/some_event.html.erb +3 -0
- data/testing/views/email/some_event.txt.erb +2 -0
- data/testing/views/layouts/layout.html.erb +1 -1
- data/testing/views/simple/default.html.erb +1 -0
- metadata +85 -24
@@ -8,12 +8,15 @@ module Noticent
|
|
8
8
|
attr_reader :notifiers
|
9
9
|
attr_reader :config
|
10
10
|
attr_reader :products
|
11
|
+
attr_reader :constructor_name
|
11
12
|
|
12
|
-
def initialize(config, name:, scope:)
|
13
|
+
def initialize(config, name:, scope:, constructor_name:)
|
13
14
|
@config = config
|
14
15
|
@name = name
|
15
16
|
@scope = scope
|
17
|
+
@constructor_name = constructor_name
|
16
18
|
@products = Noticent::Definitions::ProductGroup.new(@config)
|
19
|
+
@defaults = { _any_: Noticent::Definitions::Alert::DefaultValue.new(self, :_any_, config.default_value) }
|
17
20
|
end
|
18
21
|
|
19
22
|
def notify(recipient, template: '')
|
@@ -27,21 +30,54 @@ module Noticent
|
|
27
30
|
alert_notifier
|
28
31
|
end
|
29
32
|
|
33
|
+
def default_for(channel)
|
34
|
+
raise ArgumentError, "no channel named '#{channel}' found" if @config.channels[channel].nil?
|
35
|
+
|
36
|
+
@defaults[channel].nil? ? @defaults[:_any_].value : @defaults[channel].value
|
37
|
+
end
|
38
|
+
|
39
|
+
def default_value
|
40
|
+
@defaults[:_any_].value
|
41
|
+
end
|
42
|
+
|
43
|
+
def default(value, &block)
|
44
|
+
defaults = @defaults
|
45
|
+
|
46
|
+
if block_given?
|
47
|
+
default = Noticent::Definitions::Alert::DefaultValue.new(self, :_any_, value)
|
48
|
+
default.instance_eval(&block)
|
49
|
+
|
50
|
+
defaults[default.channel] = default
|
51
|
+
else
|
52
|
+
defaults[:_any_].value = value
|
53
|
+
end
|
54
|
+
|
55
|
+
@defaults = defaults
|
56
|
+
|
57
|
+
default
|
58
|
+
end
|
59
|
+
|
30
60
|
def applies
|
31
61
|
@products
|
32
62
|
end
|
33
63
|
|
34
64
|
def validate!
|
35
65
|
channels = @config.alert_channels(@name)
|
66
|
+
raise BadConfiguration, "no notifiers are assigned to alert '#{@name}'" if @notifiers.nil? || @notifiers.empty?
|
67
|
+
|
36
68
|
channels.each do |channel|
|
37
69
|
raise BadConfiguration, "channel #{channel.name} (#{channel.klass}) has no method called #{@name}" unless channel.klass.method_defined? @name
|
38
70
|
end
|
71
|
+
|
72
|
+
# if a payload class is available, we can make sure it has a constructor with the name of the event
|
73
|
+
raise Noticent::BadConfiguration, "payload #{@scope.payload_class} doesn't have a class method called #{name}" if @scope.check_constructor && !@scope.payload_class.respond_to?(@constructor_name)
|
39
74
|
end
|
40
75
|
|
41
76
|
# holds a list of recipient + channel
|
42
77
|
class Notifier
|
43
78
|
attr_reader :recipient
|
44
|
-
attr_reader :channel_group
|
79
|
+
attr_reader :channel_group # group to be notified
|
80
|
+
attr_reader :channel # channel to be notified
|
45
81
|
attr_reader :template
|
46
82
|
|
47
83
|
def initialize(alert, recipient, template: '')
|
@@ -50,13 +86,51 @@ module Noticent
|
|
50
86
|
@config = alert.config
|
51
87
|
@template = template
|
52
88
|
@channel_group = :default
|
89
|
+
@channel = nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def on(channel_group_or_name)
|
93
|
+
# is it a group or a channel name?
|
94
|
+
if @config.channel_groups.include? channel_group_or_name
|
95
|
+
# it's a group
|
96
|
+
@channel_group = channel_group_or_name
|
97
|
+
@channel = nil
|
98
|
+
elsif !@config.channels[channel_group_or_name].nil?
|
99
|
+
@channel_group = :_none_
|
100
|
+
@channel = @config.channels[channel_group_or_name]
|
101
|
+
else
|
102
|
+
# not a group and not a channel
|
103
|
+
raise ArgumentError, "no channel or channel group found named '#{channel_group_or_name}'"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# returns an array of all channels this notifier should send to
|
108
|
+
def applicable_channels
|
109
|
+
if @channel_group == :_none_
|
110
|
+
# it's a single channel
|
111
|
+
[@channel]
|
112
|
+
else
|
113
|
+
@config.channels_by_group(@channel_group)
|
114
|
+
end
|
53
115
|
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class DefaultValue
|
119
|
+
attr_reader :channel
|
120
|
+
attr_accessor :value
|
121
|
+
|
122
|
+
def initialize(alert, channel, value)
|
123
|
+
@alert = alert
|
124
|
+
@channel = channel
|
125
|
+
@value = value
|
126
|
+
end
|
127
|
+
|
128
|
+
def on(channel)
|
129
|
+
raise BadConfiguration, "no channel named '#{channel}'" if @alert.config.channels[channel].nil?
|
54
130
|
|
55
|
-
|
56
|
-
# validate the group name
|
57
|
-
raise ArgumentError, "no channel group found named '#{channel_group}'" if @config.channels_by_group(channel_group).empty?
|
131
|
+
@channel = channel
|
58
132
|
|
59
|
-
|
133
|
+
self
|
60
134
|
end
|
61
135
|
end
|
62
136
|
end
|
@@ -9,11 +9,17 @@ module Noticent
|
|
9
9
|
attr_reader :options
|
10
10
|
|
11
11
|
def initialize(config, name, group: :default, klass: nil)
|
12
|
+
raise BadConfiguration, 'name should be a symbol' unless name.is_a? Symbol
|
13
|
+
raise BadConfiguration, '\'_any_\' is a reserved channel name' if name == :_any_
|
14
|
+
raise BadConfiguration, '\'_none_\' is a reserved channel name' if name == :_none_
|
15
|
+
|
12
16
|
@name = name
|
13
17
|
@group = group
|
14
18
|
@config = config
|
15
19
|
|
16
|
-
|
20
|
+
sub_module = @config.use_sub_modules ? '::Channels::' : '::'
|
21
|
+
suggested_class_name = @config.base_module_name + sub_module + name.to_s.camelize
|
22
|
+
|
17
23
|
@klass = klass.nil? ? suggested_class_name.camelize.constantize : klass
|
18
24
|
rescue NameError
|
19
25
|
raise Noticent::BadConfiguration, "no class found for #{suggested_class_name}"
|
@@ -37,7 +43,6 @@ module Noticent
|
|
37
43
|
rescue ArgumentError
|
38
44
|
raise Noticent::BadConfiguration, "channel #{@klass} initializer arguments are mismatching."
|
39
45
|
end
|
40
|
-
|
41
46
|
end
|
42
47
|
end
|
43
48
|
end
|
@@ -5,22 +5,26 @@ module Noticent
|
|
5
5
|
class Scope
|
6
6
|
attr_reader :name
|
7
7
|
attr_reader :payload_class
|
8
|
+
attr_reader :check_constructor
|
8
9
|
|
9
|
-
def initialize(config, name, payload_class: nil)
|
10
|
+
def initialize(config, name, payload_class: nil, check_constructor: true)
|
10
11
|
@config = config
|
11
12
|
@name = name
|
12
|
-
|
13
|
-
|
13
|
+
@check_constructor = check_constructor
|
14
|
+
|
15
|
+
sub_module = @config.use_sub_modules ? '::Payloads::' : '::'
|
16
|
+
suggested_name = config.base_module_name + sub_module + "#{name.capitalize}Payload"
|
17
|
+
@payload_class = payload_class.nil? ? suggested_name.constantize : payload_class
|
14
18
|
rescue NameError
|
15
|
-
raise BadConfiguration, "scope #{
|
19
|
+
raise BadConfiguration, "scope #{suggested_name} class not found"
|
16
20
|
end
|
17
21
|
|
18
|
-
def alert(name, &block)
|
22
|
+
def alert(name, constructor_name: nil, &block)
|
19
23
|
alerts = @config.instance_variable_get(:@alerts) || {}
|
20
24
|
|
21
25
|
raise BadConfiguration, "alert '#{name}' already defined" if alerts.include? name
|
22
26
|
|
23
|
-
alert = Noticent::Definitions::Alert.new(@config, name: name, scope: self)
|
27
|
+
alert = Noticent::Definitions::Alert.new(@config, name: name, scope: self, constructor_name: constructor_name.nil? ? name : constructor_name)
|
24
28
|
@config.hooks&.run(:pre_alert_registration, alert)
|
25
29
|
alert.instance_eval(&block) if block_given?
|
26
30
|
@config.hooks&.run(:post_alert_registration, alert)
|
data/lib/noticent/dispatcher.rb
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
module Noticent
|
4
4
|
class Dispatcher
|
5
|
-
def initialize(config, alert_name, payload,
|
5
|
+
def initialize(config, alert_name, payload, configuration = {})
|
6
6
|
@config = config
|
7
7
|
@alert_name = alert_name
|
8
8
|
@payload = payload
|
9
|
-
@
|
9
|
+
@configuration = configuration
|
10
10
|
|
11
11
|
validate!
|
12
12
|
|
@@ -34,8 +34,8 @@ module Noticent
|
|
34
34
|
|
35
35
|
# only returns recipients that have opted-in for this channel
|
36
36
|
def filter_recipients(recipients, channel)
|
37
|
-
raise ArgumentError,
|
38
|
-
raise ArgumentError,
|
37
|
+
raise ArgumentError, "channel should be a string or symbol" unless channel.is_a?(String) || channel.is_a?(Symbol)
|
38
|
+
raise ArgumentError, "recipients is nil" if recipients.nil?
|
39
39
|
|
40
40
|
recipients.select { |recipient| @config.opt_in_provider.opted_in?(recipient_id: recipient.id, scope: scope.name, entity_id: @entity_id, alert_name: alert.name, channel_name: channel) }
|
41
41
|
end
|
@@ -43,9 +43,15 @@ module Noticent
|
|
43
43
|
def dispatch
|
44
44
|
notifiers.values.each do |notifier|
|
45
45
|
recs = recipients(notifier.recipient)
|
46
|
-
|
46
|
+
notifier.applicable_channels.each do |channel|
|
47
47
|
to_send = filter_recipients(recs, channel.name)
|
48
|
-
|
48
|
+
|
49
|
+
if to_send.count == 0 && @config.skip_alert_with_no_subscribers
|
50
|
+
Noticent.configuration.logger.info "Skipping notification of alert #{alert.name} on channel #{channel.name} as there are no subscribers"
|
51
|
+
next
|
52
|
+
end
|
53
|
+
|
54
|
+
channel_instance = channel.instance(@config, to_send, @payload, @configuration)
|
49
55
|
begin
|
50
56
|
raise Noticent::BadConfiguration, "channel #{channel.name} (#{channel.klass}) doesn't have a method called #{alert.name}" unless channel_instance.respond_to? alert.name
|
51
57
|
|
@@ -54,7 +60,7 @@ module Noticent
|
|
54
60
|
# log and move on
|
55
61
|
raise if @config.halt_on_error
|
56
62
|
|
57
|
-
Noticent.logger.error e
|
63
|
+
Noticent.configuration.logger.error e
|
58
64
|
end
|
59
65
|
end
|
60
66
|
end
|
@@ -63,12 +69,12 @@ module Noticent
|
|
63
69
|
private
|
64
70
|
|
65
71
|
def validate!
|
66
|
-
raise Noticent::BadConfiguration,
|
72
|
+
raise Noticent::BadConfiguration, "no base_dir defined" if @config.base_dir.nil?
|
67
73
|
raise Noticent::MissingConfiguration if @config.nil?
|
68
74
|
raise Noticent::BadConfiguration if @config.alerts.nil?
|
69
75
|
raise Noticent::InvalidAlert, "no alert #{@alert_name} found" if @config.alerts[@alert_name].nil?
|
70
|
-
raise ::ArgumentError,
|
71
|
-
raise ::ArgumentError,
|
76
|
+
raise ::ArgumentError, "payload is nil" if @payload.nil?
|
77
|
+
raise ::ArgumentError, "alert is not a symbol" unless @alert_name.is_a?(Symbol)
|
72
78
|
raise Noticent::BadConfiguration, "payload (#{@payload.class}) doesn't belong to this scope (#{scope.name}) as it requires #{scope.payload_class}" unless !scope.payload_class.nil? && @payload.is_a?(scope.payload_class)
|
73
79
|
raise Noticent::BadConfiguration, "payload doesn't have a #{scope.name}_id method" unless @payload.respond_to?("#{scope.name}_id")
|
74
80
|
end
|
data/lib/noticent/version.rb
CHANGED
data/lib/noticent/view.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "yaml"
|
4
4
|
|
5
5
|
module Noticent
|
6
6
|
class View
|
@@ -16,38 +16,40 @@ module Noticent
|
|
16
16
|
attr_reader :raw_data # frontmatter in their raw (pre render) format
|
17
17
|
attr_reader :rendered_data # frontmatter rendered in string format
|
18
18
|
|
19
|
-
def initialize(filename, template_filename:
|
19
|
+
def initialize(filename, template_filename: "", channel:)
|
20
20
|
raise ViewNotFound, "view #{filename} not found" unless File.exist?(filename)
|
21
|
-
raise ViewNotFound, "template #{template_filename} not found" if template_filename !=
|
22
|
-
raise ArgumentError,
|
21
|
+
raise ViewNotFound, "template #{template_filename} not found" if template_filename != "" && !File.exist?(template_filename)
|
22
|
+
raise ArgumentError, "channel is nil" if channel.nil?
|
23
23
|
|
24
24
|
@filename = filename
|
25
25
|
@view_content = File.read(filename)
|
26
|
-
@template_content = template_filename !=
|
27
|
-
@template_filename = template_filename !=
|
26
|
+
@template_content = template_filename != "" ? File.read(template_filename) : "<%= @content %>"
|
27
|
+
@template_filename = template_filename != "" ? template_filename : ""
|
28
28
|
@channel = channel
|
29
29
|
end
|
30
30
|
|
31
|
-
def process
|
31
|
+
def process(context)
|
32
32
|
parse
|
33
|
-
|
34
|
-
render_data
|
33
|
+
render_data(context)
|
35
34
|
read_data
|
35
|
+
# TODO this is nasty. we need to refactor to have an independent render context which somehow merges the binding with the channel.
|
36
|
+
@channel.data = @data
|
37
|
+
render_content(context)
|
36
38
|
end
|
37
39
|
|
38
40
|
private
|
39
41
|
|
40
|
-
def render_content
|
41
|
-
@content = @channel.render_within_context(@template_content, @
|
42
|
+
def render_content(context)
|
43
|
+
@content = @channel.render_within_context(template: @template_content, content: @raw_content, context: context)
|
42
44
|
end
|
43
45
|
|
44
|
-
def render_data
|
46
|
+
def render_data(context)
|
45
47
|
if @raw_data.nil?
|
46
48
|
@rendered_data = nil
|
47
49
|
return
|
48
50
|
end
|
49
51
|
|
50
|
-
@rendered_data = @channel.render_within_context(nil, @raw_data)
|
52
|
+
@rendered_data = @channel.render_within_context(template: nil, content: @raw_data, context: context)
|
51
53
|
end
|
52
54
|
|
53
55
|
def parse
|
@@ -70,7 +72,7 @@ module Noticent
|
|
70
72
|
if @raw_data.nil?
|
71
73
|
@data = nil
|
72
74
|
else
|
73
|
-
raise ArgumentError,
|
75
|
+
raise ArgumentError, "read_data was called before rendering" if @rendered_data.nil?
|
74
76
|
|
75
77
|
data = ::YAML.safe_load(@rendered_data)
|
76
78
|
@data = data.deep_symbolize_keys
|
data/noticent.gemspec
CHANGED
@@ -1,36 +1,42 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
lib = File.expand_path(
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
4
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
-
require
|
5
|
+
require "noticent/version"
|
6
6
|
|
7
7
|
Gem::Specification.new do |spec|
|
8
|
-
spec.name
|
9
|
-
spec.version
|
10
|
-
spec.authors
|
11
|
-
spec.email
|
8
|
+
spec.name = "noticent"
|
9
|
+
spec.version = Noticent::VERSION
|
10
|
+
spec.authors = ["Khash Sajadi"]
|
11
|
+
spec.email = ["khash@cloud66.com"]
|
12
|
+
spec.license = "Apache-2.0"
|
13
|
+
spec.homepage = "https://github.com/cloud66-oss/noticent"
|
12
14
|
|
13
|
-
spec.summary
|
15
|
+
spec.summary = "Act as Notified is a flexible framework to add notifications to a Rails application"
|
14
16
|
|
15
17
|
# Specify which files should be added to the gem when it is released.
|
16
18
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
17
19
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
18
20
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
19
21
|
end
|
20
|
-
spec.bindir
|
21
|
-
spec.executables
|
22
|
-
spec.require_paths = [
|
22
|
+
spec.bindir = "exe"
|
23
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
|
+
spec.require_paths = ["lib"]
|
23
25
|
|
24
|
-
spec.add_dependency
|
25
|
-
spec.add_dependency
|
26
|
+
spec.add_dependency "activerecord", "~> 6.0"
|
27
|
+
spec.add_dependency "activesupport", "~> 6.0"
|
28
|
+
spec.add_dependency "actionpack", "~> 6.0"
|
26
29
|
|
27
|
-
spec.add_development_dependency
|
28
|
-
spec.add_development_dependency
|
29
|
-
spec.add_development_dependency
|
30
|
-
spec.add_development_dependency
|
31
|
-
spec.add_development_dependency
|
32
|
-
spec.add_development_dependency
|
33
|
-
spec.add_development_dependency
|
34
|
-
spec.add_development_dependency
|
35
|
-
spec.add_development_dependency
|
30
|
+
spec.add_development_dependency "combustion", "~> 1.3"
|
31
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
32
|
+
spec.add_development_dependency "factory_bot", "~> 6.1"
|
33
|
+
spec.add_development_dependency "generator_spec", "~> 0.9"
|
34
|
+
spec.add_development_dependency "rails", "~> 6.0"
|
35
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
36
|
+
spec.add_development_dependency "rspec", "~> 3.10"
|
37
|
+
spec.add_development_dependency "rubocop", "~> 1.4"
|
38
|
+
spec.add_development_dependency "rubocop-performance", "~> 1.9"
|
39
|
+
spec.add_development_dependency "rufo", "~> 0.12"
|
40
|
+
spec.add_development_dependency "sqlite3", "~> 1.4"
|
41
|
+
spec.add_development_dependency "byebug", "~> 11.1"
|
36
42
|
end
|
data/testing/channels/email.rb
CHANGED
@@ -3,6 +3,16 @@ module Noticent
|
|
3
3
|
# this is a example payload for comment as scope
|
4
4
|
class CommentPayload < Noticent::Testing::Payload
|
5
5
|
attr_accessor :comment_id
|
6
|
+
attr_reader :users
|
7
|
+
|
8
|
+
def self.three
|
9
|
+
# nop
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.new_signup
|
13
|
+
# nop
|
14
|
+
end
|
15
|
+
|
6
16
|
end
|
7
17
|
end
|
8
18
|
end
|
@@ -16,6 +16,50 @@ module Noticent
|
|
16
16
|
[]
|
17
17
|
end
|
18
18
|
|
19
|
+
def self.some_event(some_value)
|
20
|
+
# nop
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.foo(options)
|
24
|
+
# nop
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.boo
|
28
|
+
# nop
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.one
|
32
|
+
# nop
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.only_here
|
36
|
+
# nop
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.two
|
40
|
+
# nop
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.tfa_enabled
|
44
|
+
# nop
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.sign_up
|
48
|
+
# nop
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.new_signup
|
52
|
+
# nop
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.fuzz
|
56
|
+
# nop
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.buzz
|
60
|
+
# nop
|
61
|
+
end
|
62
|
+
|
19
63
|
end
|
20
64
|
end
|
21
65
|
end
|