informator 0.1.0 → 1.0.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 +4 -4
- data/.travis.yml +6 -0
- data/CHANGELOG.md +31 -0
- data/README.md +114 -46
- data/Rakefile +1 -1
- data/config/metrics/metric_fu.yml +2 -2
- data/config/metrics/rubocop.yml +4 -0
- data/informator.gemspec +12 -9
- data/lib/informator.rb +3 -73
- data/lib/informator/event.rb +35 -30
- data/lib/informator/publisher.rb +103 -0
- data/lib/informator/rspec.rb +3 -0
- data/lib/informator/subscriber.rb +14 -28
- data/lib/informator/version.rb +1 -1
- data/lib/rspec/shared.rb +104 -0
- data/spec/integration/en.yml +6 -0
- data/spec/integration/fr.yml +6 -0
- data/spec/integration/i18n.rb +21 -0
- data/spec/integration/rspec_spec.rb +51 -0
- data/spec/unit/informator/event_spec.rb +82 -28
- data/spec/unit/informator/publisher_spec.rb +168 -0
- data/spec/unit/informator/subscriber_spec.rb +26 -40
- metadata +59 -11
- data/lib/informator/reporter.rb +0 -67
- data/spec/unit/informator/reporter_spec.rb +0 -75
- data/spec/unit/informator_spec.rb +0 -153
@@ -0,0 +1,103 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Informator
|
4
|
+
|
5
|
+
# The base class for publishers
|
6
|
+
#
|
7
|
+
# @api public
|
8
|
+
#
|
9
|
+
class Publisher
|
10
|
+
|
11
|
+
include Comparable
|
12
|
+
|
13
|
+
# @!attribute [r] subscribers
|
14
|
+
#
|
15
|
+
# @return [Array<Informator::Subscriber>] The list of subscribers
|
16
|
+
#
|
17
|
+
attr_reader :subscribers
|
18
|
+
|
19
|
+
# @!attribute [r] attributes
|
20
|
+
#
|
21
|
+
# @return [Hash] Initialized attributes
|
22
|
+
#
|
23
|
+
attr_reader :attributes
|
24
|
+
|
25
|
+
# @!scope class
|
26
|
+
# @!method new(attributes = {})
|
27
|
+
# Creates the immutable publisher
|
28
|
+
#
|
29
|
+
# @param [Hash] attributes
|
30
|
+
#
|
31
|
+
# @return [Informator::Publisher]
|
32
|
+
|
33
|
+
# @private
|
34
|
+
def initialize(attributes = {})
|
35
|
+
@attributes = Hash[attributes]
|
36
|
+
@subscribers = block_given? ? yield : Set.new
|
37
|
+
IceNine.deep_freeze(self)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns a new publisher with the listener being added to its subscribers
|
41
|
+
#
|
42
|
+
# @param [Object] listener
|
43
|
+
# @param [Symbol, String] callback
|
44
|
+
# The name of the listener method, that should receive events
|
45
|
+
# The method is expected to accept one argument (for the event)
|
46
|
+
#
|
47
|
+
# @return (see .new)
|
48
|
+
#
|
49
|
+
def subscribe(listener, callback)
|
50
|
+
subscriber = Subscriber.new(listener, callback)
|
51
|
+
self.class.new(attributes) { subscribers | [subscriber] }
|
52
|
+
end
|
53
|
+
|
54
|
+
# Creates the immutable event and sends it to all subscribers
|
55
|
+
#
|
56
|
+
# @param [#to_sym] name The name of the event
|
57
|
+
# @param [Hash] arguments The arguments of the event
|
58
|
+
#
|
59
|
+
# @return [Informator::Event] The published event
|
60
|
+
#
|
61
|
+
def publish(name, arguments)
|
62
|
+
event = Event.new(self, name, arguments)
|
63
|
+
subscribers.each { |subscriber| subscriber.notify(event) }
|
64
|
+
event
|
65
|
+
end
|
66
|
+
|
67
|
+
# Does the same as [#publish], and then throws the `:published` exception,
|
68
|
+
# that carries an event
|
69
|
+
#
|
70
|
+
# @param (see #publish)
|
71
|
+
#
|
72
|
+
# @return (see #publish)
|
73
|
+
#
|
74
|
+
# @raise [UncaughtThrowError] The exception to be catched later
|
75
|
+
#
|
76
|
+
def publish!(name, arguments)
|
77
|
+
event = publish(name, arguments)
|
78
|
+
throw :published, event
|
79
|
+
end
|
80
|
+
|
81
|
+
# Treats two publishers of the same class with the same attributes as equal
|
82
|
+
#
|
83
|
+
# @param [Object] other
|
84
|
+
#
|
85
|
+
# @return [Boolean]
|
86
|
+
#
|
87
|
+
def ==(other)
|
88
|
+
other.instance_of?(self.class) && attributes.eql?(other.attributes)
|
89
|
+
end
|
90
|
+
alias_method :eql?, :==
|
91
|
+
|
92
|
+
# Human-readable description of the publisher
|
93
|
+
#
|
94
|
+
# @return [String]
|
95
|
+
#
|
96
|
+
def inspect
|
97
|
+
"#<#{self.class} @attributes=#{attributes}>"
|
98
|
+
end
|
99
|
+
alias_method :to_s, :inspect
|
100
|
+
|
101
|
+
end # class Publisher
|
102
|
+
|
103
|
+
end # module Informator
|
@@ -2,64 +2,50 @@
|
|
2
2
|
|
3
3
|
module Informator
|
4
4
|
|
5
|
-
#
|
6
|
-
# to receive event notifications.
|
7
|
-
#
|
8
|
-
# @example The method [#notify] sends event to the [#object] via [#callback]
|
9
|
-
# object = Struct.new(:event).new
|
10
|
-
# subscriber = Subscriber.new object, :event=
|
11
|
-
# subscriber.frozen? # => true
|
12
|
-
#
|
13
|
-
# event = Informator::Event.new :success
|
14
|
-
# # => #<Event @type=:success @attributes={} @messages=[]>
|
15
|
-
#
|
16
|
-
# subscriber.notify event
|
17
|
-
# object.event
|
18
|
-
# # => #<Event @type=:success @attributes={} @messages=[]>
|
5
|
+
# Describes a subscriber for publisher's notifications
|
19
6
|
#
|
20
7
|
# @api private
|
21
8
|
#
|
22
9
|
class Subscriber
|
23
10
|
|
24
|
-
include Equalizer.new(:
|
11
|
+
include Equalizer.new(:listener, :callback)
|
25
12
|
|
26
|
-
# @!attribute [r]
|
13
|
+
# @!attribute [r] listener
|
27
14
|
#
|
28
|
-
# @return [Object]
|
15
|
+
# @return [Object] The listener to send events to
|
29
16
|
#
|
30
|
-
attr_reader :
|
17
|
+
attr_reader :listener
|
31
18
|
|
32
19
|
# @!attribute [r] callback
|
33
20
|
#
|
34
|
-
# @return [Symbol]
|
21
|
+
# @return [Symbol] The name of the listener's method that receives events
|
35
22
|
#
|
36
23
|
attr_reader :callback
|
37
24
|
|
38
25
|
# @!scope class
|
39
|
-
# @!method new(
|
40
|
-
# Builds the subscriber for given
|
26
|
+
# @!method new(listener, callback)
|
27
|
+
# Builds the subscriber for given listener and callback
|
41
28
|
#
|
42
|
-
# @param [Object]
|
43
|
-
# @param [#to_sym] callback
|
29
|
+
# @param [Object] listener
|
30
|
+
# @param [#to_sym] callback
|
44
31
|
#
|
45
32
|
# @return [Informator::Subscriber]
|
46
33
|
|
47
34
|
# @private
|
48
|
-
def initialize(
|
49
|
-
@
|
35
|
+
def initialize(listener, callback)
|
36
|
+
@listener = listener
|
50
37
|
@callback = callback.to_sym
|
51
38
|
IceNine.deep_freeze(self)
|
52
39
|
end
|
53
40
|
|
54
|
-
# Sends the event to the subscriber
|
41
|
+
# Sends the event to the subscriber listener via its callback
|
55
42
|
#
|
56
43
|
# @param [Informator::Event] event
|
57
44
|
#
|
58
45
|
# @return [Informator::Event] published event
|
59
46
|
#
|
60
47
|
def notify(event)
|
61
|
-
|
62
|
-
|
48
|
+
listener.public_send callback, event
|
63
49
|
event
|
64
50
|
end
|
65
51
|
|
data/lib/informator/version.rb
CHANGED
data/lib/rspec/shared.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
shared_context :event_translations do
|
4
|
+
|
5
|
+
let(:__locale__) { defined?(locale) ? locale : :en }
|
6
|
+
|
7
|
+
around do |example|
|
8
|
+
old, I18n.locale = I18n.locale, __locale__
|
9
|
+
example.run
|
10
|
+
I18n.locale = old
|
11
|
+
end
|
12
|
+
|
13
|
+
end # shared context
|
14
|
+
|
15
|
+
# @example
|
16
|
+
# it_behaves_like :publishing_event do
|
17
|
+
#
|
18
|
+
# # required settings for the specification
|
19
|
+
# subject { MyPublisher.new(attributes).subscribe(listener, callback).call }
|
20
|
+
# let(:listener) { double callback => nil } # required
|
21
|
+
# let(:callback) { "foo" } # required
|
22
|
+
#
|
23
|
+
# # optional settings
|
24
|
+
# let(:event) { Event.new publisher, :success, exclamation: "Wow" }
|
25
|
+
# let(:event_name) { :success } # The name of the event
|
26
|
+
# let(:event_attributes) { { exclamation: "Wow" } } # The attributes for the event
|
27
|
+
# let(:locale) { :en } # :en by default
|
28
|
+
# let(:event_message) { "Wow, success!" } # The message
|
29
|
+
#
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
shared_examples :publishing_event do
|
33
|
+
|
34
|
+
include_context :event_translations
|
35
|
+
|
36
|
+
before do
|
37
|
+
fail SyntaxError.new "subject should be defined" unless defined? subject
|
38
|
+
fail SyntaxError.new "listener should be defined" unless defined? listener
|
39
|
+
fail SyntaxError.new "callback should be defined" unless defined? callback
|
40
|
+
end
|
41
|
+
|
42
|
+
it "[publishes event]" do
|
43
|
+
expect(listener).to receive(callback) do |received|
|
44
|
+
if defined? event
|
45
|
+
expect(received).to eql(event), <<-REPORT.gsub(/ *\|/, "")
|
46
|
+
|
|
47
|
+
|#{listener}##{callback} should have received the event:
|
48
|
+
|
|
49
|
+
| expected: #{event.inspect}
|
50
|
+
| got: #{received.inspect}
|
51
|
+
REPORT
|
52
|
+
else
|
53
|
+
expect(received)
|
54
|
+
.to be_kind_of(Informator::Event), <<-REPORT.gsub(/ *\|/, "")
|
55
|
+
|
|
56
|
+
|#{listener}##{callback} should have received an event.
|
57
|
+
|
|
58
|
+
| expected: instance of Informator::Event subclass
|
59
|
+
| got: #{received.inspect}
|
60
|
+
REPORT
|
61
|
+
|
62
|
+
if defined? event_name
|
63
|
+
expect(received.name)
|
64
|
+
.to eql(event_name), <<-REPORT.gsub(/ *\|/, "")
|
65
|
+
|
|
66
|
+
|#{listener}##{callback} should have received an event with the name:
|
67
|
+
|
|
68
|
+
| expected: #{event_name}
|
69
|
+
| got: #{received.name}
|
70
|
+
|from event: #{received.inspect}
|
71
|
+
REPORT
|
72
|
+
end
|
73
|
+
|
74
|
+
if defined? event_attributes
|
75
|
+
expect(received.attributes)
|
76
|
+
.to eql(event_attributes), <<-REPORT.gsub(/ *\|/, "")
|
77
|
+
|
|
78
|
+
|#{listener}##{callback} should have received an event with the attributes:
|
79
|
+
|
|
80
|
+
| expected: #{event_attributes}
|
81
|
+
| got: #{received.attributes}
|
82
|
+
|from event: #{received.inspect}
|
83
|
+
REPORT
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
if defined? event_message
|
88
|
+
expect(received.message)
|
89
|
+
.to eql(event_message), <<-REPORT.gsub(/ *\|/, "")
|
90
|
+
|
|
91
|
+
|Language: #{__locale__.to_s.upcase}
|
92
|
+
|#{listener}##{callback} should have received an event with the message:
|
93
|
+
|
|
94
|
+
| expected: #{event_message}
|
95
|
+
| got: #{received.message}
|
96
|
+
|from event: #{received.inspect}
|
97
|
+
REPORT
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
subject
|
102
|
+
end
|
103
|
+
|
104
|
+
end # shared examples
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
shared_context :preloaded_translations do
|
4
|
+
|
5
|
+
around do |example|
|
6
|
+
|
7
|
+
load_path = Dir[File.expand_path "../*.yml", __FILE__]
|
8
|
+
|
9
|
+
old_locale, I18n.locale = I18n.locale, :en
|
10
|
+
old_path, I18n.load_path = I18n.load_path, load_path
|
11
|
+
I18n.backend.load_translations
|
12
|
+
|
13
|
+
example.run
|
14
|
+
|
15
|
+
I18n.locale = old_locale
|
16
|
+
I18n.load_path = old_path
|
17
|
+
I18n.backend.reload!
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end # shared context
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "informator/rspec"
|
4
|
+
require "timecop"
|
5
|
+
require_relative "i18n"
|
6
|
+
|
7
|
+
describe "shared examples" do
|
8
|
+
|
9
|
+
include_context :preloaded_translations
|
10
|
+
|
11
|
+
before do
|
12
|
+
class Informator::Foo < Informator::Publisher
|
13
|
+
def call
|
14
|
+
if attributes[:exclamation] == "Wow"
|
15
|
+
publish :success, attributes
|
16
|
+
else
|
17
|
+
publish :error, attributes
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
Timecop.freeze
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:lucky) { Informator::Foo.new exclamation: "Wow" }
|
25
|
+
let(:unlucky) { Informator::Foo.new exclamation: "OMG" }
|
26
|
+
|
27
|
+
let(:listener) { double callback => nil, freeze: nil }
|
28
|
+
let(:callback) { :call }
|
29
|
+
|
30
|
+
it_behaves_like :publishing_event do
|
31
|
+
subject { lucky.subscribe(listener, callback).call }
|
32
|
+
|
33
|
+
let(:event) { Informator::Event.new lucky, :success, exclamation: "Wow" }
|
34
|
+
let(:event_message) { "publisher said: Wow!" }
|
35
|
+
end
|
36
|
+
|
37
|
+
it_behaves_like :publishing_event do
|
38
|
+
subject { unlucky.subscribe(listener, callback).call }
|
39
|
+
let(:locale) { :fr }
|
40
|
+
|
41
|
+
let(:event_name) { :error }
|
42
|
+
let(:event_attributes) { { exclamation: "OMG" } }
|
43
|
+
let(:event_message) { "éditeur dit: OMG!" }
|
44
|
+
end
|
45
|
+
|
46
|
+
after do
|
47
|
+
Timecop.return
|
48
|
+
Informator.send :remove_const, :Foo
|
49
|
+
end
|
50
|
+
|
51
|
+
end # describe shared examples
|
@@ -1,81 +1,135 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
require "timecop"
|
4
|
+
|
3
5
|
describe Informator::Event do
|
4
6
|
|
5
|
-
|
7
|
+
let!(:pub_class) { Informator::Foo = Class.new }
|
8
|
+
let(:publisher) { pub_class.new }
|
9
|
+
let(:name) { "success" }
|
10
|
+
let(:attributes) { { foo: :FOO } }
|
11
|
+
let(:event) { described_class.new publisher, name, attributes }
|
6
12
|
|
7
|
-
|
13
|
+
before { Timecop.freeze }
|
14
|
+
after { Timecop.return }
|
8
15
|
|
9
16
|
describe ".new" do
|
10
17
|
|
11
|
-
|
12
|
-
|
13
|
-
end # describe .new
|
14
|
-
|
15
|
-
describe ".[]" do
|
18
|
+
subject { event }
|
16
19
|
|
17
|
-
|
20
|
+
it { is_expected.to be_frozen }
|
18
21
|
|
19
|
-
it "
|
20
|
-
expect
|
21
|
-
described_class[:foo, :bar, :baz]
|
22
|
+
it "doesn't freeze attributes" do
|
23
|
+
expect { subject }.not_to change { attributes.frozen? }
|
22
24
|
end
|
23
25
|
|
24
26
|
end # describe .new
|
25
27
|
|
26
|
-
describe "#
|
28
|
+
describe "#publisher" do
|
29
|
+
|
30
|
+
subject { event.publisher }
|
31
|
+
|
32
|
+
it { is_expected.to eql publisher }
|
33
|
+
it { is_expected.to be_frozen }
|
27
34
|
|
28
|
-
|
29
|
-
it { is_expected.to eql type.to_sym }
|
35
|
+
end # describe #publisher
|
30
36
|
|
31
|
-
|
37
|
+
describe "#name" do
|
32
38
|
|
33
|
-
|
39
|
+
subject { event.name }
|
34
40
|
|
35
|
-
|
36
|
-
it { is_expected.to eql %w(foo bar) }
|
41
|
+
it { is_expected.to eql name.to_sym }
|
37
42
|
|
38
|
-
end # describe #
|
43
|
+
end # describe #name
|
39
44
|
|
40
45
|
describe "#attributes" do
|
41
46
|
|
42
47
|
subject { event.attributes }
|
43
|
-
|
48
|
+
|
49
|
+
it { is_expected.to eql attributes }
|
50
|
+
it { is_expected.to be_frozen }
|
44
51
|
|
45
52
|
end # describe #attributes
|
46
53
|
|
54
|
+
describe "#time" do
|
55
|
+
|
56
|
+
subject { event.time }
|
57
|
+
|
58
|
+
it "returns the time of event" do
|
59
|
+
expect(subject).to eql Time.now
|
60
|
+
end
|
61
|
+
|
62
|
+
end # describe #time
|
63
|
+
|
64
|
+
describe "#message" do
|
65
|
+
|
66
|
+
subject { event.message }
|
67
|
+
|
68
|
+
it do
|
69
|
+
is_expected
|
70
|
+
.to eql "translation missing: en.informator.informator/foo.success"
|
71
|
+
end
|
72
|
+
|
73
|
+
it "sends event attributes to translator" do
|
74
|
+
expect(I18n).to receive(:translate) do |_, opts|
|
75
|
+
expect(opts.merge(attributes)).to eql opts
|
76
|
+
end
|
77
|
+
subject
|
78
|
+
end
|
79
|
+
|
80
|
+
it { is_expected.to be_frozen }
|
81
|
+
|
82
|
+
end # describe #message
|
83
|
+
|
47
84
|
describe "#==" do
|
48
85
|
|
49
86
|
subject { event == other }
|
50
87
|
|
51
|
-
context "
|
88
|
+
context "with the same publisher, times, names and attributes" do
|
52
89
|
|
53
|
-
let(:other) {
|
90
|
+
let(:other) { described_class.new publisher, name, attributes }
|
54
91
|
it { is_expected.to eql true }
|
55
92
|
|
56
93
|
end # context
|
57
94
|
|
58
|
-
context "
|
95
|
+
context "with different publishers" do
|
96
|
+
|
97
|
+
let(:new_publisher) { Informator::Publisher.new }
|
59
98
|
|
60
|
-
let(:other) { described_class.new
|
99
|
+
let(:other) { described_class.new new_publisher, name, attributes }
|
61
100
|
it { is_expected.to eql false }
|
62
101
|
|
63
102
|
end # context
|
64
103
|
|
65
|
-
context "
|
104
|
+
context "with different times" do
|
105
|
+
|
106
|
+
let!(:event) { described_class.new publisher, name, attributes }
|
107
|
+
let!(:other) do
|
108
|
+
new_time = Time.now + 1
|
109
|
+
Timecop.travel(new_time)
|
110
|
+
described_class.new publisher, name, attributes
|
111
|
+
end
|
66
112
|
|
67
|
-
let(:other) { described_class.new :success, baz: "qux" }
|
68
113
|
it { is_expected.to eql false }
|
69
114
|
|
70
115
|
end # context
|
71
116
|
|
72
|
-
context "
|
117
|
+
context "with different names" do
|
73
118
|
|
74
|
-
let(:other) {
|
119
|
+
let(:other) { described_class.new publisher, "other", attributes }
|
120
|
+
it { is_expected.to eql false }
|
121
|
+
|
122
|
+
end # context
|
123
|
+
|
124
|
+
context "with different attributes" do
|
125
|
+
|
126
|
+
let(:other) { described_class.new publisher, "other" }
|
75
127
|
it { is_expected.to eql false }
|
76
128
|
|
77
129
|
end # context
|
78
130
|
|
79
131
|
end # describe #==
|
80
132
|
|
133
|
+
after { Informator.send :remove_const, :Foo }
|
134
|
+
|
81
135
|
end # describe Informator::Event
|