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.
@@ -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