dry-events 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8df0a07c20aa79e2b47d1f8eda583936b6109e09e5307826593e1e8f792c7675
4
+ data.tar.gz: c99dfee0bf7614b62fc70d8ea809d419fcf8fb01400149f83610fd9a9e586019
5
+ SHA512:
6
+ metadata.gz: 3060f0fe13e7cb9199e81e82c82be67bca90a547a0750b094f0afcdcb193aba0866c3c3a7af2e81923dec194bc1828d9fc4ae7fa099c0c87952cba68f77afff2
7
+ data.tar.gz: 33845156cc553098260f562001b3fa0d2aa711c95ee8d056c0f1362eccda69e8b8954040bf0b542b47826a81b813d1a338ced05712e10e36a8770fad4eef867f
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ .DS_Store
2
+ coverage
3
+ /.bundle
4
+ vendor/bundle
5
+ bin/
6
+ tmp/
7
+ .idea/
8
+ Gemfile.lock
9
+ spec/test_logs/*.log
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --require spec_helper
3
+ --order random
data/.travis.yml ADDED
@@ -0,0 +1,22 @@
1
+ language: ruby
2
+ cache: bundler
3
+ bundler_args: --without benchmarks console tools
4
+ script: "bundle exec rake ci"
5
+ before_install: gem update --system
6
+ after_success:
7
+ - '[ "${TRAVIS_JOB_NUMBER#*.}" = "1" ] && [ "$TRAVIS_BRANCH" = "master" ] && bundle exec codeclimate-test-reporter'
8
+ rvm:
9
+ - 2.5.0
10
+ - 2.4.3
11
+ - 2.3.6
12
+ - jruby-9.1.9.0
13
+ env:
14
+ global:
15
+ - COVERAGE='true'
16
+ notifications:
17
+ webhooks:
18
+ urls:
19
+ - https://webhooks.gitter.im/e/19098b4253a72c9796db
20
+ on_success: change
21
+ on_failure: always
22
+ on_start: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # v0.1.0 to-be-released
2
+
3
+ First public release
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,29 @@
1
+ # Issue Guidelines
2
+
3
+ ## Reporting bugs
4
+
5
+ If you found a bug, report an issue and describe what's the expected behavior versus what actually happens. If the bug causes a crash, attach a full backtrace. If possible, a reproduction script showing the problem is highly appreciated.
6
+
7
+ ## Reporting feature requests
8
+
9
+ Report a feature request **only after discussing it first on [discuss.dry-rb.org](https://discuss.dry-rb.org)** where it was accepted. Please provide a concise description of the feature, don't link to a discussion thread, and instead summarize what was discussed.
10
+
11
+ ## Reporting questions, support requests, ideas, concerns etc.
12
+
13
+ **PLEASE DON'T** - use [discuss.dry-rb.org](http://discuss.dry-rb.org) instead.
14
+
15
+ # Pull Request Guidelines
16
+
17
+ A Pull Request will only be accepted if it addresses a specific issue that was reported previously, or fixes typos, mistakes in documentation etc.
18
+
19
+ Other requirements:
20
+
21
+ 1) Do not open a pull request if you can't provide tests along with it. If you have problems writing tests, ask for help in the related issue.
22
+ 2) Follow the style conventions of the surrounding code. In most cases, this is standard ruby style.
23
+ 3) Add API documentation if it's a new feature
24
+ 4) Update API documentation if it changes an existing feature
25
+ 5) Bonus points for sending a PR to [github.com/dry-rb/dry-rb.org](github.com/dry-rb/dry-rb.org) which updates user documentation and guides
26
+
27
+ # Asking for help
28
+
29
+ If these guidelines aren't helpful, and you're stuck, please post a message on [discuss.dry-rb.org](https://discuss.dry-rb.org).
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'rack'
7
+ gem 'simplecov', platform: :mri, require: false
8
+ gem 'codeclimate-test-reporter', require: false
9
+ end
10
+
11
+ group :tools do
12
+ gem 'byebug', platform: :mri
13
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 dry-rb team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,26 @@
1
+ [gem]: https://rubygems.org/gems/dry-events
2
+ [travis]: https://travis-ci.org/dry-rb/dry-events
3
+ [gemnasium]: https://gemnasium.com/dry-rb/dry-events
4
+ [codeclimate]: https://codeclimate.com/github/dry-rb/dry-events
5
+ [coveralls]: https://coveralls.io/r/dry-rb/dry-events
6
+ [inchpages]: http://inch-ci.org/github/dry-rb/dry-events
7
+
8
+ # dry-events [![Join the chat at https://gitter.im/dry-rb/chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/dry-rb/chat)
9
+
10
+ [![Gem Version](https://badge.fury.io/rb/dry-events.svg)][gem]
11
+ [![Build Status](https://travis-ci.org/dry-rb/dry-events.svg?branch=master)][travis]
12
+ [![Dependency Status](https://gemnasium.com/dry-rb/dry-events.svg)][gemnasium]
13
+ [![Code Climate](https://codeclimate.com/github/dry-rb/dry-events/badges/gpa.svg)][codeclimate]
14
+ [![Test Coverage](https://codeclimate.com/github/dry-rb/dry-events/badges/coverage.svg)][codeclimate]
15
+ [![Inline docs](http://inch-ci.org/github/dry-rb/dry-events.svg?branch=master)][inchpages]
16
+
17
+ Standalone pub/sub system.
18
+
19
+ ## Synopsis
20
+
21
+ ``` ruby
22
+ ```
23
+
24
+ ## License
25
+
26
+ See `LICENSE` file.
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
5
+
6
+ require 'rspec/core'
7
+ require 'rspec/core/rake_task'
8
+
9
+ task default: :spec
10
+
11
+ desc 'Run all specs in spec directory'
12
+ RSpec::Core::RakeTask.new(:spec)
13
+
14
+ desc "Run CI tasks"
15
+ task ci: [:spec]
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ require File.expand_path('../lib/dry/events/version', __FILE__)
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = 'dry-events'
6
+ spec.version = Dry::Events::VERSION
7
+ spec.authors = ['Piotr Solnica']
8
+ spec.email = ['piotr.solnica+oss@gmail.com']
9
+ spec.summary = 'Pub/sub system'
10
+ spec.homepage = 'https://github.com/dry-rb/dry-events'
11
+ spec.license = 'MIT'
12
+
13
+ spec.files = `git ls-files -z`.split("\x0")
14
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
15
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
+ spec.require_paths = ['lib']
17
+
18
+ spec.add_runtime_dependency 'concurrent-ruby', '~> 1.0'
19
+ spec.add_runtime_dependency 'dry-core', '~> 0.4'
20
+ spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
21
+
22
+ spec.add_development_dependency 'bundler'
23
+ spec.add_development_dependency 'rake'
24
+ spec.add_development_dependency 'rspec'
25
+ end
data/lib/dry-events.rb ADDED
@@ -0,0 +1 @@
1
+ require 'dry/events'
data/lib/dry/events.rb ADDED
@@ -0,0 +1 @@
1
+ require 'dry/events/publisher'
@@ -0,0 +1,82 @@
1
+ require 'dry/events/constants'
2
+
3
+ module Dry
4
+ module Events
5
+ # Event bus
6
+ #
7
+ # An event bus stores listeners (callbacks) and events
8
+ #
9
+ # @api private
10
+ class Bus
11
+ # @!attribute [r] events
12
+ # @return [Hash] A hash with events registered within a bus
13
+ attr_reader :events
14
+
15
+ # @!attribute [r] listeners
16
+ # @return [Hash] A hash with event listeners registered within a bus
17
+ attr_reader :listeners
18
+
19
+ # Initialize a new event bus
20
+ #
21
+ # @param [Symbol] id The bus identifier
22
+ # @param [Hash] events A hash with events
23
+ # @param [Hash] listeners A hash with listeners
24
+ #
25
+ # @api private
26
+ def initialize(events: EMPTY_HASH, listeners: LISTENERS_HASH.dup)
27
+ @listeners = listeners
28
+ @events = events
29
+ end
30
+
31
+ # @api private
32
+ def process(event_id, payload, &block)
33
+ listeners[event_id].each do |(listener, query)|
34
+ event = events[event_id].payload(payload)
35
+
36
+ if event.trigger?(query)
37
+ yield(event, listener)
38
+ end
39
+ end
40
+ end
41
+
42
+ # @api private
43
+ def publish(event_id, payload)
44
+ process(event_id, payload) do |event, listener|
45
+ listener.(event)
46
+ end
47
+ end
48
+
49
+ # @api private
50
+ def attach(listener, query)
51
+ events.each do |id, event|
52
+ meth = event.listener_method
53
+
54
+ if listener.respond_to?(meth)
55
+ listeners[id] << [listener.method(meth), query]
56
+ end
57
+ end
58
+ end
59
+
60
+ # @api private
61
+ def detach(listener)
62
+ listeners.each do |id, memo|
63
+ memo.each do |tuple|
64
+ current_listener, _ = tuple
65
+ listeners[id].delete(tuple) if current_listener.receiver.equal?(listener)
66
+ end
67
+ end
68
+ end
69
+
70
+ # @api private
71
+ def subscribe(event_id, query, &block)
72
+ listeners[event_id] << [block, query]
73
+ self
74
+ end
75
+
76
+ # @api private
77
+ def subscribed?(listener)
78
+ listeners.values.any? { |value| value.any? { |(block, _)| block.equal?(listener) } }
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,10 @@
1
+ require 'concurrent/map'
2
+ require 'dry/core/constants'
3
+
4
+ module Dry
5
+ module Events
6
+ include Dry::Core::Constants
7
+
8
+ LISTENERS_HASH = Concurrent::Map.new { |h, k| h[k] = [] }
9
+ end
10
+ end
@@ -0,0 +1,81 @@
1
+ require 'dry/equalizer'
2
+ require 'dry/events/constants'
3
+
4
+ module Dry
5
+ module Events
6
+ # Event object
7
+ #
8
+ # @api public
9
+ class Event
10
+ include Dry::Equalizer(:id, :payload)
11
+
12
+ DOT = '.'.freeze
13
+ UNDERSCORE = '_'.freeze
14
+
15
+ # @!attribute [r] id
16
+ # @return [Symbol] The event identifier
17
+ attr_reader :id
18
+
19
+ # Initialize a new event
20
+ #
21
+ # @param [Symbol] id The event identifier
22
+ # @param [Hash] payload Optional payload
23
+ #
24
+ # @return [Event]
25
+ #
26
+ # @api private
27
+ def initialize(id, payload = EMPTY_HASH)
28
+ @id = id
29
+ @payload = payload
30
+ end
31
+
32
+ # Get data from the payload
33
+ #
34
+ # @param [String,Symbol] name
35
+ #
36
+ # @api public
37
+ def [](name)
38
+ @payload.fetch(name)
39
+ end
40
+
41
+ # Coerce an event to a hash
42
+ #
43
+ # @return [Hash]
44
+ #
45
+ # @api public
46
+ def to_h
47
+ @payload
48
+ end
49
+ alias_method :to_hash, :to_h
50
+
51
+ # Get or set a payload
52
+ #
53
+ # @overload
54
+ # @return [Hash] payload
55
+ #
56
+ # @overload payload(data)
57
+ # @param [Hash] data A new payload
58
+ # @return [Event] A copy of the event with the provided payload
59
+ #
60
+ # @api public
61
+ def payload(data = nil)
62
+ if data
63
+ self.class.new(id, @payload.merge(data))
64
+ else
65
+ @payload
66
+ end
67
+ end
68
+
69
+ # @api private
70
+ def trigger?(query)
71
+ query.empty? || query.all? { |key, value| payload[key] == value }
72
+ end
73
+
74
+ # @api private
75
+ def listener_method
76
+ @listener_method ||= :"on_#{id.to_s.gsub(DOT, UNDERSCORE)}"
77
+ end
78
+ end
79
+ end
80
+ end
81
+
@@ -0,0 +1,57 @@
1
+ require 'dry/equalizer'
2
+ require 'dry/events/publisher'
3
+
4
+ module Dry
5
+ module Events
6
+ # Extension for objects that can listen to events
7
+ #
8
+ # @example
9
+ # class AppEvents
10
+ # include Dry::Events::Publisher[:app]
11
+ #
12
+ # register_event("users.created")
13
+ # end
14
+
15
+ # class MyListener
16
+ # include Dry::Events::Listener[:app]
17
+ #
18
+ # subscribe("users.created") do |event|
19
+ # # do something
20
+ # end
21
+ # end
22
+ #
23
+ # @api public
24
+ class Listener < Module
25
+ include Dry::Equalizer(:id)
26
+
27
+ # @!attribute [r] :id
28
+ # @return [Symbol,String] The publisher identifier
29
+ # @api private
30
+ attr_reader :id
31
+
32
+ # Create a listener extension for a specific publisher
33
+ #
34
+ # @return [Module]
35
+ #
36
+ # @api public
37
+ def self.[](id)
38
+ new(id)
39
+ end
40
+
41
+ # @api private
42
+ def initialize(id)
43
+ @id = id
44
+
45
+ define_method(:subscribe) do |event_id, query = EMPTY_HASH, &block|
46
+ Publisher.registry[id].subscribe(event_id, query, &block)
47
+ end
48
+ end
49
+
50
+ # @api private
51
+ def included(klass)
52
+ klass.extend(self)
53
+ super
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,242 @@
1
+ require 'concurrent/map'
2
+
3
+ require 'dry/core/class_attributes'
4
+
5
+ require 'dry/events/constants'
6
+ require 'dry/events/event'
7
+ require 'dry/events/bus'
8
+
9
+ module Dry
10
+ module Events
11
+ # Exception raised when the same publisher is registered more than once
12
+ #
13
+ # @api public
14
+ PublisherAlreadyRegisteredError = Class.new(StandardError) do
15
+ # @api private
16
+ def initialize(id)
17
+ super("publisher with id #{id.inspect} already registered as: #{Publisher.registry[id]}")
18
+ end
19
+ end
20
+
21
+ # Extension used for classes that can publish events
22
+ #
23
+ # @example
24
+ # class AppEvents
25
+ # include Dry::Events::Publisher[:app]
26
+ #
27
+ # register_event('users.created')
28
+ # end
29
+ #
30
+ # class CreateUser
31
+ # attr_reader :events
32
+ #
33
+ # def initialize(events)
34
+ # @events = events
35
+ # end
36
+ #
37
+ # def call(user)
38
+ # # do your thing
39
+ # events.publish('users.created', user: user, time: Time.now)
40
+ # end
41
+ # end
42
+ #
43
+ # app_events = AppEvents.new
44
+ # create_user = CreateUser.new(app_events)
45
+ #
46
+ # # this will publish "users.created" event with its payload
47
+ # create_user.call(name: "Jane")
48
+ #
49
+ # @api public
50
+ class Publisher < Module
51
+ include Dry::Equalizer(:id)
52
+
53
+ # Internal publisher registry, which is used to identify them globally
54
+ #
55
+ # This allows us to have listener classes that can subscribe to events
56
+ # without having access to instances of publishers yet.
57
+ #
58
+ # @api private
59
+ def self.registry
60
+ @__registry__ ||= Concurrent::Map.new
61
+ end
62
+
63
+ # @!attribute [r] :id
64
+ # @return [Symbol,String] the publisher identifier
65
+ # @api private
66
+ attr_reader :id
67
+
68
+ # Create a publisher extension with the provided identifier
69
+ #
70
+ # @param [Symbol,String] id The identifier
71
+ #
72
+ # @return [Publisher]
73
+ #
74
+ # @raise PublisherAlreadyRegisteredError
75
+ #
76
+ # @api public
77
+ def self.[](id)
78
+ raise PublisherAlreadyRegisteredError.new(id) if registry.key?(id)
79
+ new(id)
80
+ end
81
+
82
+ # @api private
83
+ def initialize(id)
84
+ @id = id
85
+ end
86
+
87
+ # Hook for inclusions/extensions
88
+ #
89
+ # It registers the publisher class under global registry using the id
90
+ #
91
+ # @api private
92
+ def included(klass)
93
+ klass.extend(ClassMethods)
94
+ klass.include(InstanceMethods)
95
+
96
+ self.class.registry[id] = klass
97
+
98
+ super
99
+ end
100
+
101
+ # Class interface for publisher classes
102
+ #
103
+ # @api public
104
+ module ClassMethods
105
+ # Register an event
106
+ #
107
+ # @param [String] event_id The event identifier
108
+ # @param [Hash] payload Optional default payload
109
+ #
110
+ # @api public
111
+ def register_event(event_id, payload = EMPTY_HASH)
112
+ events[event_id] = Event.new(event_id, payload)
113
+ self
114
+ end
115
+
116
+ # Subscribe to an event
117
+ #
118
+ # @param [Symbol,String] event_id The event identifier
119
+ # @param [Hash] query An optional query for conditional listeners
120
+ #
121
+ # @return [Class] publisher class
122
+ #
123
+ # @api public
124
+ def subscribe(event_id, query = EMPTY_HASH, &block)
125
+ listeners[event_id] << [block, query]
126
+ self
127
+ end
128
+
129
+ # Sets up event bus for publisher instances
130
+ #
131
+ # @return [Bus]
132
+ #
133
+ # @api private
134
+ def new_bus
135
+ Bus.new(events: events.dup, listeners: listeners.dup)
136
+ end
137
+
138
+ # Global registry with events
139
+ #
140
+ # @api private
141
+ def events
142
+ @__events__ ||= Concurrent::Map.new
143
+ end
144
+
145
+ # Global registry with listeners
146
+ #
147
+ # @api private
148
+ def listeners
149
+ @__listeners__ ||= LISTENERS_HASH.dup
150
+ end
151
+ end
152
+
153
+ # Instance interface for publishers
154
+ #
155
+ # @api public
156
+ module InstanceMethods
157
+ # Register a new event type at instance level
158
+ #
159
+ # @param [Symbol,String] event_id The event identifier
160
+ # @param [Hash] payload Optional default payload
161
+ #
162
+ # @return [self]
163
+ #
164
+ # @api public
165
+ def register_event(event_id, payload = EMPTY_HASH)
166
+ __bus__.events[event_id] = Event.new(event_id, payload)
167
+ self
168
+ end
169
+ # Publish an event
170
+ #
171
+ # @param [String] event_id The event identifier
172
+ # @param [Hash] payload An optional payload
173
+ #
174
+ # @api public
175
+ def publish(event_id, payload = EMPTY_HASH)
176
+ __bus__.publish(event_id, payload)
177
+ self
178
+ end
179
+ alias_method :trigger, :publish
180
+
181
+ # Subscribe to events.
182
+ #
183
+ # If the query parameter is provided, filters events by payload.
184
+ #
185
+ # @param [Symbol,String,Object] object_or_event_id The event identifier or a listener object
186
+ # @param [Hash] query An optional event filter
187
+ #
188
+ # @return [Object] self
189
+ #
190
+ # @api public
191
+ def subscribe(object_or_event_id, query = EMPTY_HASH, &block)
192
+ if block
193
+ __bus__.subscribe(object_or_event_id, query, &block)
194
+ else
195
+ __bus__.attach(object_or_event_id, query)
196
+ end
197
+ self
198
+ end
199
+
200
+ # Unsubscribe a listener
201
+ #
202
+ # @param [Object] listener The listener object
203
+ #
204
+ # @return [self]
205
+ #
206
+ # @api public
207
+ def unsubscribe(listener)
208
+ __bus__.detach(listener)
209
+ end
210
+
211
+ # Return true if a given listener has been subscribed to any event
212
+ #
213
+ # @api public
214
+ def subscribed?(listener)
215
+ __bus__.subscribed?(listener)
216
+ end
217
+
218
+ # Utility method which yields event with each of its listeners
219
+ #
220
+ # Listeners are already filtered out when query was provided during
221
+ # subscription
222
+ #
223
+ # @param [Symbol,String] event_id The event identifier
224
+ # param [Hash] payload An optional payload
225
+ #
226
+ # @api public
227
+ def process(event_id, payload = EMPTY_HASH, &block)
228
+ __bus__.process(event_id, payload, &block)
229
+ end
230
+
231
+ # Internal event bus
232
+ #
233
+ # @return [Bus]
234
+ #
235
+ # @api private
236
+ def __bus__
237
+ @__bus__ ||= self.class.new_bus
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,5 @@
1
+ module Dry
2
+ module Events
3
+ VERSION = '0.1.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,25 @@
1
+ if RUBY_ENGINE == 'ruby' && ENV['COVERAGE'] == 'true'
2
+ require 'simplecov'
3
+ SimpleCov.start do
4
+ add_filter '/spec/'
5
+ end
6
+ end
7
+
8
+ begin
9
+ require 'byebug'
10
+ rescue LoadError; end
11
+
12
+ require 'dry-events'
13
+
14
+ SPEC_ROOT = Pathname(__dir__)
15
+
16
+ Dir[SPEC_ROOT.join('shared/**/*.rb')].each(&method(:require))
17
+ Dir[SPEC_ROOT.join('support/**/*.rb')].each(&method(:require))
18
+
19
+ RSpec.configure do |config|
20
+ config.disable_monkey_patching!
21
+
22
+ config.after(:example) do
23
+ Dry::Events::Publisher.instance_variable_set(:@__registry__, Concurrent::Map.new)
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ require 'dry/events/listener'
2
+
3
+ RSpec.describe Dry::Events::Listener do
4
+ subject(:listener) do
5
+ Class.new {
6
+ include Dry::Events::Listener[:test_publisher]
7
+ }
8
+ end
9
+
10
+ let!(:publisher) do
11
+ Class.new {
12
+ include Dry::Events::Publisher[:test_publisher]
13
+
14
+ register_event :test_event
15
+ }.new
16
+ end
17
+
18
+ describe '.subscribe' do
19
+ it 'subscribes a listener at class level' do
20
+ result = []
21
+
22
+ listener.subscribe(:test_event) do |event|
23
+ result << event.id
24
+ end
25
+
26
+ publisher.publish(:test_event)
27
+
28
+ expect(result).to eql([:test_event])
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,125 @@
1
+ require 'dry/events/publisher'
2
+
3
+ RSpec.describe Dry::Events::Publisher do
4
+ subject(:publisher) do
5
+ Class.new {
6
+ include Dry::Events::Publisher[:test_publisher]
7
+
8
+ register_event :test_event
9
+ }.new
10
+ end
11
+
12
+ describe '.[]' do
13
+ it 'creates a publisher extension with provided id' do
14
+ publisher = Class.new do
15
+ include Dry::Events::Publisher[:my_publisher]
16
+ end
17
+
18
+ expect(Dry::Events::Publisher.registry[:my_publisher]).to be(publisher)
19
+ end
20
+
21
+ it 'does not allow same id to be used for than once' do
22
+ create_publisher = -> do
23
+ Class.new do
24
+ include Dry::Events::Publisher[:my_publisher]
25
+ end
26
+ end
27
+
28
+ create_publisher.()
29
+
30
+ expect { create_publisher.() }.to raise_error(Dry::Events::PublisherAlreadyRegisteredError, /my_publisher/)
31
+ end
32
+ end
33
+
34
+ describe '.subscribe' do
35
+ it 'subscribes a listener at class level' do
36
+ listener = -> * { }
37
+
38
+ publisher.class.subscribe(:test_event, &listener)
39
+
40
+ expect(publisher.subscribed?(listener)).to be(true)
41
+ end
42
+ end
43
+
44
+ describe '#register_event' do
45
+ it 'registers a new event at instance level' do
46
+ listener = -> * { }
47
+
48
+ publisher.register_event(:test_another_event).subscribe(:test_another_event, &listener)
49
+
50
+ expect(publisher.subscribed?(listener)).to be(true)
51
+ end
52
+ end
53
+
54
+ describe '#subscribe' do
55
+ it 'subscribes a listener function' do
56
+ listener = -> * { }
57
+
58
+ publisher.subscribe(:test_event, &listener)
59
+
60
+ expect(publisher.subscribed?(listener)).to be(true)
61
+ end
62
+
63
+ it 'subscribes a listener object' do
64
+ listener = Class.new do
65
+ attr_reader :captured
66
+
67
+ def initialize
68
+ @captured = []
69
+ end
70
+
71
+ def on_test_event(event)
72
+ captured << event[:message]
73
+ end
74
+ end.new
75
+
76
+ publisher.subscribe(listener).publish(:test_event, message: 'it works')
77
+
78
+ expect(listener.captured).to eql(['it works'])
79
+
80
+ publisher.unsubscribe(listener)
81
+
82
+ publisher.publish(:test_event, message: 'it works')
83
+
84
+ expect(listener.captured).to eql(['it works'])
85
+ end
86
+ end
87
+
88
+ describe '#publish' do
89
+ it 'publishes an event' do
90
+ result = []
91
+ listener = -> event { result << event[:message] }
92
+
93
+ publisher.subscribe(:test_event, &listener).publish(:test_event, message: 'it works')
94
+
95
+ expect(result).to eql(['it works'])
96
+ end
97
+
98
+ it 'publishes an event filtered by a query' do
99
+ result = []
100
+ listener = -> test: { result << test }
101
+
102
+ publisher.
103
+ subscribe(:test_event, test: true, &listener).
104
+ publish(:test_event, test: false).
105
+ publish(:test_event, test: true)
106
+
107
+ expect(result).to eql([true])
108
+ end
109
+ end
110
+
111
+ describe '#process' do
112
+ it 'yields event and its listeners' do
113
+ result = []
114
+ listener = -> event { result << event.id }
115
+
116
+ publisher.subscribe(:test_event, &listener)
117
+
118
+ publisher.process(:test_event) do |event, listener|
119
+ listener.(event)
120
+ end
121
+
122
+ expect(result).to eql([:test_event])
123
+ end
124
+ end
125
+ end
metadata ADDED
@@ -0,0 +1,152 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dry-events
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Piotr Solnica
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-01-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: concurrent-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-core
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: dry-equalizer
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description:
98
+ email:
99
+ - piotr.solnica+oss@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rspec"
106
+ - ".travis.yml"
107
+ - CHANGELOG.md
108
+ - CONTRIBUTING.md
109
+ - Gemfile
110
+ - LICENSE
111
+ - README.md
112
+ - Rakefile
113
+ - dry-events.gemspec
114
+ - lib/dry-events.rb
115
+ - lib/dry/events.rb
116
+ - lib/dry/events/bus.rb
117
+ - lib/dry/events/constants.rb
118
+ - lib/dry/events/event.rb
119
+ - lib/dry/events/listener.rb
120
+ - lib/dry/events/publisher.rb
121
+ - lib/dry/events/version.rb
122
+ - spec/spec_helper.rb
123
+ - spec/unit/dry/events/listener_spec.rb
124
+ - spec/unit/dry/events/publisher_spec.rb
125
+ homepage: https://github.com/dry-rb/dry-events
126
+ licenses:
127
+ - MIT
128
+ metadata: {}
129
+ post_install_message:
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ requirements: []
144
+ rubyforge_project:
145
+ rubygems_version: 2.7.3
146
+ signing_key:
147
+ specification_version: 4
148
+ summary: Pub/sub system
149
+ test_files:
150
+ - spec/spec_helper.rb
151
+ - spec/unit/dry/events/listener_spec.rb
152
+ - spec/unit/dry/events/publisher_spec.rb