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 +7 -0
- data/.gitignore +9 -0
- data/.rspec +3 -0
- data/.travis.yml +22 -0
- data/CHANGELOG.md +3 -0
- data/CONTRIBUTING.md +29 -0
- data/Gemfile +13 -0
- data/LICENSE +20 -0
- data/README.md +26 -0
- data/Rakefile +15 -0
- data/dry-events.gemspec +25 -0
- data/lib/dry-events.rb +1 -0
- data/lib/dry/events.rb +1 -0
- data/lib/dry/events/bus.rb +82 -0
- data/lib/dry/events/constants.rb +10 -0
- data/lib/dry/events/event.rb +81 -0
- data/lib/dry/events/listener.rb +57 -0
- data/lib/dry/events/publisher.rb +242 -0
- data/lib/dry/events/version.rb +5 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/unit/dry/events/listener_spec.rb +31 -0
- data/spec/unit/dry/events/publisher_spec.rb +125 -0
- metadata +152 -0
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
data/.rspec
ADDED
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
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
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 [](https://gitter.im/dry-rb/chat)
|
9
|
+
|
10
|
+
[][gem]
|
11
|
+
[][travis]
|
12
|
+
[][gemnasium]
|
13
|
+
[][codeclimate]
|
14
|
+
[][codeclimate]
|
15
|
+
[][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]
|
data/dry-events.gemspec
ADDED
@@ -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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|