informator 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|