informator 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+
2
+ # This file added to allow `require "informator/rspec"`
3
+ require_relative "../rspec/shared"
@@ -2,64 +2,50 @@
2
2
 
3
3
  module Informator
4
4
 
5
- # Class Subscriber wraps the [#object] along with its [#callback] method
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(:object, :callback)
11
+ include Equalizer.new(:listener, :callback)
25
12
 
26
- # @!attribute [r] object
13
+ # @!attribute [r] listener
27
14
  #
28
- # @return [Object] the object to send events to
15
+ # @return [Object] The listener to send events to
29
16
  #
30
- attr_reader :object
17
+ attr_reader :listener
31
18
 
32
19
  # @!attribute [r] callback
33
20
  #
34
- # @return [Symbol] the name of the object methods to listen to events
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(object, callback)
40
- # Builds the subscriber for given object and callback
26
+ # @!method new(listener, callback)
27
+ # Builds the subscriber for given listener and callback
41
28
  #
42
- # @param [Object] object
43
- # @param [#to_sym] callback (:receive)
29
+ # @param [Object] listener
30
+ # @param [#to_sym] callback
44
31
  #
45
32
  # @return [Informator::Subscriber]
46
33
 
47
34
  # @private
48
- def initialize(object, callback = :receive)
49
- @object = object
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 object via its callback
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
- object.public_send callback, event
62
-
48
+ listener.public_send callback, event
63
49
  event
64
50
  end
65
51
 
@@ -4,6 +4,6 @@ module Informator
4
4
 
5
5
  # The semantic version of the module.
6
6
  # @see http://semver.org/ Semantic versioning 2.0
7
- VERSION = "0.1.0".freeze
7
+ VERSION = "1.0.0".freeze
8
8
 
9
9
  end # module Informator
@@ -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,6 @@
1
+ ---
2
+ en:
3
+ informator:
4
+ informator/foo:
5
+ success: "publisher said: %{exclamation}!"
6
+ error: "publisher said: %{exclamation}!"
@@ -0,0 +1,6 @@
1
+ ---
2
+ fr:
3
+ informator:
4
+ informator/foo:
5
+ success: "éditeur dit: %{exclamation}!"
6
+ error: "éditeur dit: %{exclamation}!"
@@ -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
- subject(:event) { described_class.new type, :foo, [:bar], baz: :qux }
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
- let(:type) { "success" }
13
+ before { Timecop.freeze }
14
+ after { Timecop.return }
8
15
 
9
16
  describe ".new" do
10
17
 
11
- it { is_expected.to be_frozen }
12
-
13
- end # describe .new
14
-
15
- describe ".[]" do
18
+ subject { event }
16
19
 
17
- before { allow(described_class).to receive(:new) }
20
+ it { is_expected.to be_frozen }
18
21
 
19
- it "builds the event" do
20
- expect(described_class).to receive(:new).with(:foo, :bar, :baz)
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 "#type" do
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
- subject { event.type }
29
- it { is_expected.to eql type.to_sym }
35
+ end # describe #publisher
30
36
 
31
- end # describe #type
37
+ describe "#name" do
32
38
 
33
- describe "#messages" do
39
+ subject { event.name }
34
40
 
35
- subject { event.messages }
36
- it { is_expected.to eql %w(foo bar) }
41
+ it { is_expected.to eql name.to_sym }
37
42
 
38
- end # describe #messages
43
+ end # describe #name
39
44
 
40
45
  describe "#attributes" do
41
46
 
42
47
  subject { event.attributes }
43
- it { is_expected.to eq(baz: :qux) }
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 "to event with the same type and attributes" do
88
+ context "with the same publisher, times, names and attributes" do
52
89
 
53
- let(:other) { Class.new(described_class).new type, baz: :qux }
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 "to event with another type" do
95
+ context "with different publishers" do
96
+
97
+ let(:new_publisher) { Informator::Publisher.new }
59
98
 
60
- let(:other) { described_class.new :error, baz: :qux }
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 "to event with other attributes" do
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 "to non-event" do
117
+ context "with different names" do
73
118
 
74
- let(:other) { :foo }
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