emittance 1.1.0 → 2.0.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: bdfbafff04e9e3f13600cd866e8feceea7f0d5e3
4
- data.tar.gz: 58a17c61fc1861ad40799d774c0423b11a1d1f8f
2
+ SHA256:
3
+ metadata.gz: 9ef6d6a01910b713c52f871249fffbddcd3534818b7b92640de2de23b9b1d838
4
+ data.tar.gz: 55a9f2bd1d5734a4171d42db6098bb0dba27a26cae0071a43712dff4d34011b1
5
5
  SHA512:
6
- metadata.gz: 54f8e3827b60740b23a26d6edff782bd46bcb8d2f1144fd29548f7c17aec0402323311b2644d28e2f7520c437a6d3a40667be73edbbdbd2660f2d3ee5103e140
7
- data.tar.gz: 8acf082bf58a7fe57d23e239ebfe283fe33a4888e9653783509079f2b2a7acfcedbcc551f5b657d9f498227285d22e10cfc9817ae82c30737fc55d35c90ad9c2
6
+ metadata.gz: e8475538f43bdfe7d0a524270a7f69c31ecd7f90a19e7146899a0f286606d8bf68f087c6ac40d321b02c5fc38b279217fb648b4352c693168814c0715f420ed8
7
+ data.tar.gz: f3b5f39f6ba0a5f311378e21d6e09158048c57c8f0b4c22f11a16c15146a82e85c17acac4f4c181acd4c78d6dc18a9215d1081933e1855a5f45478c0f17185e3
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.4.2
1
+ 2.5.3
data/.travis.yml CHANGED
@@ -3,7 +3,10 @@ env:
3
3
  - secure: "uBY5L4Qs7b9dEVG2XAJ2x9O+PgJTIA9Ezkiu7KrfxjNhMViSUOGmkKvVxBWKwFzChfsFT2wQK/TYcNNtpZoFCLxqyBTCFV1DLHiPQ3mAW4yHPiRbwkEX5HSjypn+4ONd+nk26hMgfrnnAoHM5m4tkAQRo1dY8ARv46iH2wctErVYzj5ACf/OUmrIoF/+QXkE98oOWLMYhtyAhmlBusdcM+czreMD2BUzFilkhVLLY41KA1f7EE0W4v8aY87SOsBR6Q6bFKXm9bW3xxR6nKHukFXgXtfkkylcGOZur8VrvTxx/NAKTOyx/mCo4h1SqwZrIJQoPh3uBwv6n41YGkIzFQoebPiUnnsVYCnlz0V2AnnCdcZ/LrpIVra+bN7bh3oMtEKmlrU8cHeRQy0HlaD9u4o1KYwlg0X8lApGpsSsc+GKAC9MzGk4P3aOWFFSxqs/oo98bhlxuCvYjEh+x/aXeOqG6a/3Vlg6p/gJPxqDlaHmF6JfYGWrCysnJQtbBAstH/HhT2XHm/96uH3OURI7o+tx/I63/Qz/YG68O0nV9leFUrcmARK8XMavn1N5BM0Uoh/XhC0JUnu3GJJ7g7kABPkck81U1qWskDMeUm8hhfJgWVSixnAxKBehX+rKrizbcj3/UyqYKy7SnNHJ6fQo34rBxDLe0O8ULbWifj5/Xg4="
4
4
  language: ruby
5
5
  rvm:
6
- - 2.4.2
6
+ - 2.2.10
7
+ - 2.4.5
8
+ - 2.5.4
9
+ - 2.6.2
7
10
  before_script:
8
11
  - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
9
12
  - chmod +x ./cc-test-reporter
data/.yardopts CHANGED
@@ -2,3 +2,5 @@
2
2
  --no-private
3
3
  'lib/**/*.rb'
4
4
  --readme README.md
5
+ -
6
+ docs/RoutingStrategies.md
data/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # emittance changelog
2
+
3
+ ## Unreleased
4
+ - Drop support for ruby versions < 2.2
5
+ - Add support for multiple brokers
6
+ - Add the "topical" routing strategy, which mimicks RabbitMQ's topic queue routing
data/Gemfile.lock CHANGED
@@ -6,20 +6,27 @@ PATH
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
+ ast (2.4.0)
9
10
  coderay (1.1.2)
10
11
  diff-lcs (1.3)
11
12
  docile (1.1.5)
13
+ jaro_winkler (1.5.2)
12
14
  json (2.1.0)
13
15
  method_source (0.9.0)
16
+ parallel (1.14.0)
17
+ parser (2.6.0.0)
18
+ ast (~> 2.4.0)
19
+ powerpack (0.1.2)
14
20
  pry (0.11.3)
15
21
  coderay (~> 1.1.0)
16
22
  method_source (~> 0.9.0)
23
+ rainbow (3.0.0)
17
24
  rake (10.5.0)
18
25
  rspec (3.7.0)
19
26
  rspec-core (~> 3.7.0)
20
27
  rspec-expectations (~> 3.7.0)
21
28
  rspec-mocks (~> 3.7.0)
22
- rspec-core (3.7.0)
29
+ rspec-core (3.7.1)
23
30
  rspec-support (~> 3.7.0)
24
31
  rspec-expectations (3.7.0)
25
32
  diff-lcs (>= 1.2.0, < 2.0)
@@ -28,11 +35,21 @@ GEM
28
35
  diff-lcs (>= 1.2.0, < 2.0)
29
36
  rspec-support (~> 3.7.0)
30
37
  rspec-support (3.7.0)
38
+ rubocop (0.62.0)
39
+ jaro_winkler (~> 1.5.1)
40
+ parallel (~> 1.10)
41
+ parser (>= 2.5, != 2.5.1.1)
42
+ powerpack (~> 0.1)
43
+ rainbow (>= 2.2.2, < 4.0)
44
+ ruby-progressbar (~> 1.7)
45
+ unicode-display_width (~> 1.4.0)
46
+ ruby-progressbar (1.10.0)
31
47
  simplecov (0.15.1)
32
48
  docile (~> 1.1.0)
33
49
  json (>= 1.8, < 3)
34
50
  simplecov-html (~> 0.10.0)
35
51
  simplecov-html (0.10.2)
52
+ unicode-display_width (1.4.1)
36
53
  yard (0.9.12)
37
54
 
38
55
  PLATFORMS
@@ -44,8 +61,9 @@ DEPENDENCIES
44
61
  pry
45
62
  rake (~> 10.0)
46
63
  rspec (~> 3.0)
64
+ rubocop
47
65
  simplecov
48
66
  yard
49
67
 
50
68
  BUNDLED WITH
51
- 1.16.0
69
+ 1.17.2
@@ -0,0 +1,39 @@
1
+ # @title Routing Strategies
2
+
3
+ # Routing Strategies
4
+
5
+ Emittance can be configured to use one of several "routing strategies." A routing strategy is a way in which an event is identified so that watchers can decide which events they wish to subscribe to. All routing strategies encapsulate the same following basic ideas:
6
+
7
+ 1. Events have one or more "identifiers." These identifiers are meant to express the "type" of event that was emitted, such as `order_completed`, or `user_logged_in`.
8
+ 2. Watchers can choose the identifier(s) they wish to subscribe to. Some routing strategies can even allow a watcher to watch for multiple kinds of events with a single identifier. For instance, if we are using the `:topical` routing strategy, we can watch for the identifier `posts.*`, and be able to receive events with the identifier `posts.create` and `posts.destroy`.
9
+
10
+ ## Routing Strategy Architecture
11
+
12
+ All routing strategies encompass two procedures: the creation of an event, and the registration/retrieval of watcher subscriptions. If you wish to create your own routing strategy, then you must implement both.
13
+
14
+ ### Event Lookup & Creation
15
+
16
+ Event lookup is a residual feature of the "classical" routing strategy, which created a separate class for a given event identifier. This might eventually be removed in later versions, but for now a routing strategy must provide an object or class that implements three methods: `.identifiers_for_klass`, `.find_event_klass`, and `.register_identifier`.
17
+
18
+ `.identifiers_for_klass` takes an event class and (optionally) an event object, returning a list of identifiers for that given class. In the future this workflow will be simplified, but for now it is how an event's identifier is determined.
19
+
20
+ `.find_event_klass` takes an identifier or set of identifiers, returning the relevant event class for those identifiers. With future lookup strategies, this will be unnecessary being that all events will have the same class.
21
+
22
+ `.register_identifier` adds an identifier to a given event class. This is essentially a no-op with future lookup strategies, but the idea is that a given event type can have multiple identifiers.
23
+
24
+ ### Subscription Registration & Identifier Routing
25
+
26
+ The primary purpose of a routing strategy is to facilitate the registration and retrieval of subscriptions given a specific event identifier. A routing strategy provides a class the instances of which serve to store subscriptions and route queries. Such a class must implement four instance methods: `#register`, `#[]`, `#clear_registrations_for`, and `#clear`.
27
+
28
+ `#register` takes an identifier and a subscription object (subscriptions can be anything) and stores the pair for later retrieval.
29
+
30
+ `#[]` takes an identifier and returns an enumerable collection of subscriptions relevant to that particular identifier. The rules for which subscriptions are returned are up to the author. This method should also return an object which can also be used to add a subscription to the parent registry using the `#<<` method.
31
+
32
+ ```ruby
33
+ subscriptions = my_regisration_map['some_identifier']
34
+ subscriptions << another_subscription
35
+ ```
36
+
37
+ `#clear_registrations_for` takes an identifier and clears all subscriptions relevant to that identifier.
38
+
39
+ `#clear` clears all subscriptions.
data/emittance.gemspec CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
- # coding: utf-8
3
2
 
4
- lib = File.expand_path('../lib', __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
5
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
5
  require 'emittance/version'
7
6
 
@@ -27,6 +26,7 @@ Gem::Specification.new do |spec|
27
26
  spec.add_development_dependency 'pry'
28
27
  spec.add_development_dependency 'rake', '~> 10.0'
29
28
  spec.add_development_dependency 'rspec', '~> 3.0'
29
+ spec.add_development_dependency 'rubocop'
30
30
  spec.add_development_dependency 'simplecov'
31
31
  spec.add_development_dependency 'yard'
32
32
  end
@@ -5,31 +5,105 @@ module Emittance
5
5
  # The clearinghouse for brokers. Registers brokers, and decides which broker to use when sent an event. First point of
6
6
  # contact for event propagation.
7
7
  #
8
- class Brokerage
8
+ # == Multiple brokers
9
+ #
10
+ # Emittance can support multiple brokers. This is implemented in a whitelist fashion. To enable a broker, just call
11
+ # {Emittance::Brokerage.use_broker}:
12
+ #
13
+ # Emittance.use_broker :asynchronous
14
+ #
15
+ # Watchers subscribe to events on a per-broker basis. By default, a watcher will subscribe on the default broker
16
+ # Emittance initializes with +synchronous+ as the default broker). You can override that default by specifying it in
17
+ # a parameter on the {Emittance::Watcher.watch} method:
18
+ #
19
+ # MyWatcher.watch :something_cool_happened, broker: :asynchronous { |event| puts event.payload.inspect }
20
+ #
21
+ # To change the default broker, use {Emittance::Brokerage.default_broker=}:
22
+ #
23
+ # Emittance.default_broker = :asynchronous
24
+ #
25
+ module Brokerage
26
+ class BrokerNotInUseError < StandardError; end
27
+
9
28
  @enabled = true
10
- @current_broker = nil
11
29
 
12
30
  class << self
31
+ attr_reader :default_broker
32
+
33
+ # Sends an event to all brokers that are in-use.
34
+ #
13
35
  # @param event [Emittance::Event] the event object
14
- def send_event(event)
15
- broker.process_event(event) if enabled?
36
+ def send_event(event, middleware: Emittance::Middleware)
37
+ return nil unless enabled?
38
+
39
+ event = middleware.up(event)
40
+ brokers_in_use.each { |broker| broker.process_event(event) }
16
41
  end
17
42
 
18
- # @return [Class] the currently selected broker
19
- def broker
20
- @current_broker
43
+ # @return [Set<Emittance::Broker>]
44
+ def brokers_in_use
45
+ @brokers_in_use ||= Set.new
21
46
  end
22
47
 
23
- # @param identifier [Symbol] the symbol you have registered the broker to
24
- def use_broker(identifier)
25
- @current_broker = registry.fetch identifier
48
+ # Normalizes broker input in order to provide an interface that allows either a {Emittance::Broker} subclass _or_
49
+ # its identifier to be passed in to a method.
50
+ #
51
+ # @param broker [Class, Symbol, nil] either a broker or its identifier
52
+ def find_broker(broker)
53
+ if brokers_in_use.include?(broker) || (broker.is_a?(Class) && broker <= Emittance::Broker)
54
+ broker
55
+ elsif broker.nil?
56
+ default_broker
57
+ else
58
+ registry.fetch(broker)
59
+ end
26
60
  end
27
61
 
28
- # @param broker [Emittance::Broker] the broker you would like to register
29
- def register_broker(broker, symbol)
30
- registry.register broker, symbol
62
+ # Checks if a broker is in use in this brokerage.
63
+ #
64
+ # @return [Boolean] true if broker is in use, false otherwise
65
+ def broker_in_use?(broker)
66
+ broker = find_broker(broker)
67
+
68
+ brokers_in_use.include?(broker)
31
69
  end
32
70
 
71
+ # Adds a broker to the list of {Emittance::Broker} subclasses available. The first broker to be added becomes the
72
+ # default broker.
73
+ #
74
+ # @param broker [Class, Symbol] the symbol you have registered the broker to
75
+ def use_broker(broker)
76
+ broker = find_broker(broker)
77
+
78
+ brokers_in_use << broker
79
+ self.default_broker = broker unless default_broker
80
+ end
81
+
82
+ # Sets the default broker. If the watcher does not specify the broker, this will be the broker that gets
83
+ def default_broker=(broker)
84
+ broker = find_broker(broker)
85
+ raise BrokerNotInUseError, 'Default broker must be in use' unless broker_in_use?(broker)
86
+
87
+ @default_broker = broker
88
+ end
89
+
90
+ # A semi-private API. If you have created your own broker, this method adds it to the available pool of brokers.
91
+ #
92
+ # Emittance::Brokerage.register_broker MyBroker, :mine
93
+ #
94
+ # @param broker [Class] the broker you would like to register
95
+ # @param identifier [Symbol] the symbol you would like use to point to your registered broker
96
+ def register_broker(broker, identifier)
97
+ registry.register broker, identifier
98
+ end
99
+
100
+ def dispatcher_for(broker = nil)
101
+ broker = find_broker(broker)
102
+ broker.dispatcher
103
+ end
104
+
105
+ alias dispatcher dispatcher_for
106
+
33
107
  # @return [Module] the registry containing all broker registrations
34
108
  def registry
35
109
  Emittance::Brokerage::Registry
@@ -8,7 +8,7 @@ module Emittance
8
8
  # A proxy for a hash. Identifies special identifiers.
9
9
  #
10
10
  class RegistrationMap
11
- SPECIAL_IDENTIFIER_REGEX = /^\@/
11
+ SPECIAL_IDENTIFIER_REGEX = /^\@/.freeze
12
12
 
13
13
  class << self
14
14
  # @param identifier the identifier we want to know information about
@@ -38,6 +38,10 @@ module Emittance
38
38
  self
39
39
  end
40
40
 
41
+ def clear
42
+ @reg_map = {}
43
+ end
44
+
41
45
  private
42
46
 
43
47
  attr_reader :reg_map
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Emittance
6
+ class Dispatcher
7
+ ##
8
+ # Tracks registrations for a set of topics.
9
+ #
10
+ # == Structure & Registration
11
+ #
12
+ # A +TopicRegistrationMap+ is structured like a hash, where the keys are topic parts, and the values are something
13
+ # called a +Mapping+. A +Mapping+ is a struct containing a set of subscriptions for its immediate topic level, as
14
+ # well as another +TopicRegistrationMap+. This forms a tree-like structure. For example, if we have a set of
15
+ # subscriptions on the following topics (their index is denoted above each element for clarity:
16
+ #
17
+ # # 0 1 2 3 4 5 6 7 8 9 10 11 12
18
+ # routing_key = ['*', '*', '*', '*', '*.b', '*.b', 'a', 'a.*', 'a.*', 'a.*', 'a.*.c', 'a.*.c', 'a.a']
19
+ #
20
+ # We could register them each using the {#register} method:
21
+ #
22
+ # registrations = TopicRegistrationMap.new
23
+ #
24
+ # routing_key.each_with_index do |routing_key, idx|
25
+ # registrations.register(routing_key, idx)
26
+ # end
27
+ #
28
+ # The resulting map would look like this:
29
+ #
30
+ # root map:
31
+ # *:
32
+ # subscriptions: [0, 1, 2, 3]
33
+ # map:
34
+ # b:
35
+ # subscriptions: [4, 5]
36
+ # a:
37
+ # subscriptions: [6]
38
+ # map:
39
+ # *:
40
+ # subscriptions: [7, 8, 9]
41
+ # map:
42
+ # c:
43
+ # subscriptions: [10, 11]
44
+ # a:
45
+ # subscriptions: [12]
46
+ #
47
+ # == Lookup
48
+ #
49
+ # Suppose we publish to the topic +a.a+. We can use the {#[]} method to fetch all the subscriptions relevant to
50
+ # that topic:
51
+ #
52
+ # registrations['a.a'] # => [7, 8, 9, 12]
53
+ #
54
+ class TopicRegistrationMap
55
+ Subscription = Struct.new(:routing_key, :registration)
56
+
57
+ # @param root [TopicRegistrationMap] (private API)
58
+ def initialize(root = nil)
59
+ @root = root
60
+ end
61
+
62
+ # @private
63
+ def root
64
+ @root || self
65
+ end
66
+
67
+ # @private
68
+ def mappings
69
+ @mappings ||= new_mappings
70
+ end
71
+
72
+ # Looks up subscriptions whose registrations match the given topic.
73
+ #
74
+ # @param topic_or_event [#to_s, Emittance::Event] the topic or event for which you wish to look up subscriptions
75
+ # @return [Enumerable] the set of subscriptions
76
+ def [](topic_or_event)
77
+ topic, head, tail, parts = process_routing_key(topic_or_event)
78
+
79
+ items = parts.length == 1 ? my_subscriptions(head, original_lookup: topic) : child_subscriptions(head, tail)
80
+
81
+ Result.new(root, topic, items)
82
+ end
83
+
84
+ # @private
85
+ def subscriptions_for_exactly(topic_part, original_lookup:)
86
+ Result.new(root, original_lookup, mappings[topic_part].subscriptions)
87
+ end
88
+
89
+ # @private
90
+ def all_child_subscriptions_for_exactly(topic_part, original_lookup:)
91
+ mappings.values.reduce(Result.new(root, original_lookup, Set.new)) do |result, mapping|
92
+ result + mapping.map.subscriptions_for_exactly(topic_part, original_lookup: original_lookup)
93
+ end
94
+ end
95
+
96
+ # Registers a subscription to the given routing key.
97
+ #
98
+ # @param routing_key [#to_s] the routing key that you wish to subscribe to
99
+ # @param registration [Object] the registration you wish to store under that routing key
100
+ # @param original_routing_key [#to_s] (private API)
101
+ def register(routing_key, registration, original_routing_key: nil)
102
+ routing_key, head, tail, parts = process_routing_key(routing_key)
103
+ original_routing_key ||= routing_key
104
+
105
+ mapping = mappings[head]
106
+
107
+ if parts.length == 1
108
+ mapping << Subscription.new(original_routing_key, registration)
109
+ else
110
+ mapping.map.register(tail, registration, original_routing_key: original_routing_key)
111
+ end
112
+ end
113
+
114
+ # Clears registrations associated with a given routing key.
115
+ #
116
+ # @param routing_key [#to_s] the routing key the registrations for which you with to clear from this map
117
+ def clear_registrations_for(routing_key)
118
+ _routing_key, head, tail, parts = process_routing_key(routing_key)
119
+
120
+ mapping = mappings[head]
121
+
122
+ if parts.length == 1
123
+ mapping.subscriptions.clear
124
+ else
125
+ mapping.map.clear_registrations_for(tail)
126
+ end
127
+ end
128
+
129
+ def clear
130
+ @mappings = new_mappings
131
+ end
132
+
133
+ private
134
+
135
+ # rubocop:disable Metrics/AbcSize
136
+ def my_subscriptions(head, original_lookup:)
137
+ mappings['#'].subscriptions +
138
+ mappings['*'].subscriptions +
139
+ mappings[head].subscriptions +
140
+ mappings[head].map.subscriptions_for_exactly('#', original_lookup: original_lookup) +
141
+ mappings['*'].map.subscriptions_for_exactly('#', original_lookup: original_lookup) +
142
+ mappings['#'].map.subscriptions_for_exactly(head, original_lookup: original_lookup) +
143
+ mappings['#'].map.subscriptions_for_exactly('*', original_lookup: original_lookup)
144
+ end
145
+
146
+ def child_subscriptions(head, tail)
147
+ Result.new(root, tail, mappings['#'].subscriptions) +
148
+ child_subscriptions_for_hash_on_tail(tail) +
149
+ mappings['*'].map[tail] +
150
+ mappings[head].map[tail]
151
+ end
152
+
153
+ def child_subscriptions_for_hash_on_tail(routing_key)
154
+ original_routing_key = routing_key
155
+ result = Result.new(root, original_routing_key)
156
+
157
+ until parts_for_routing_key(routing_key).empty?
158
+ routing_key, _, tail, = process_routing_key(routing_key)
159
+ result += Result.new(root, original_routing_key, mappings['#'].map[routing_key].items)
160
+
161
+ routing_key = tail
162
+ end
163
+
164
+ result
165
+ end
166
+ # rubocop:enable Metrics/AbcSize
167
+
168
+ def process_routing_key(routing_key)
169
+ routing_key = normalize_routing_key(routing_key)
170
+ parts = parts_for_routing_key(routing_key)
171
+ tail = routing_key_for_parts(parts[1..-1])
172
+
173
+ [routing_key, parts.first, tail, parts]
174
+ end
175
+
176
+ def normalize_routing_key(routing_key)
177
+ if routing_key.is_a?(Emittance::Event)
178
+ routing_key.topic
179
+ elsif routing_key.to_s == '@all' # support for legacy special identifier
180
+ '#'
181
+ else
182
+ routing_key
183
+ end
184
+ end
185
+
186
+ def parts_for_routing_key(routing_key)
187
+ routing_key.to_s.split('.')
188
+ end
189
+
190
+ def routing_key_for_parts(parts)
191
+ parts.join('.')
192
+ end
193
+
194
+ def new_mappings
195
+ Hash.new { |h, k| h[k] = Mapping.new(root) }
196
+ end
197
+
198
+ # @private
199
+ class Mapping
200
+ attr_reader :root_map
201
+
202
+ def initialize(root_map)
203
+ @root_map = root_map
204
+ end
205
+
206
+ def push(new_subscription)
207
+ subscriptions << new_subscription
208
+ end
209
+
210
+ alias << push
211
+
212
+ def subscriptions
213
+ @subscriptions ||= Set.new
214
+ end
215
+
216
+ def map
217
+ @map ||= TopicRegistrationMap.new(root_map)
218
+ end
219
+ end
220
+
221
+ # @private
222
+ class Result
223
+ include Enumerable
224
+
225
+ attr_reader :root_map, :lookup_key, :items
226
+
227
+ def initialize(root_map, lookup_key, items = Set.new)
228
+ @root_map = root_map
229
+ @lookup_key = lookup_key
230
+ @items = items
231
+ end
232
+
233
+ def each
234
+ return enum_for(:each) unless block_given?
235
+
236
+ items.each { |item| item.respond_to?(:registration) ? yield(item.registration) : yield(item) }
237
+ end
238
+
239
+ # Adds a subscription to the root mapping. This is set up all wonky in this manner with a (sort of) circular
240
+ # reference because the original API was set up this way. This allows us to add subscriptions to the
241
+ # collection itself.
242
+ def push(new_subscription)
243
+ root_map.register(lookup_key, new_subscription)
244
+ end
245
+
246
+ alias << push
247
+
248
+ def +(other)
249
+ unless root_map == other.root_map && lookup_key == other.lookup_key
250
+ raise ArgumentError, 'Cannot add two Results with different root_maps or lookup_keys'
251
+ end
252
+
253
+ self.class.new(root_map, lookup_key, items + other.items)
254
+ end
255
+
256
+ def empty?
257
+ items.empty?
258
+ end
259
+
260
+ def length
261
+ items.length
262
+ end
263
+
264
+ alias size length
265
+ alias count length
266
+
267
+ def first
268
+ items.first
269
+ end
270
+
271
+ def last
272
+ items.last
273
+ end
274
+
275
+ def clear
276
+ root_map.clear_registrations_for(lookup_key)
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
@@ -3,10 +3,11 @@
3
3
  require 'set'
4
4
 
5
5
  require 'emittance/dispatcher/registration_map'
6
+ require 'emittance/dispatcher/topic_registration_map'
6
7
 
7
8
  module Emittance
8
9
  ##
9
- # Abstract class for dispatchers. Subclasses must implement the following methods:
10
+ # Abstract class for dispatchers. Subclasses must implement the following class methods:
10
11
  #
11
12
  # - +._process_event+
12
13
  # - +._register+
@@ -17,11 +18,65 @@ module Emittance
17
18
  # you want, but typically represent the callback you would like to run whenever an event of a certain type is
18
19
  # emitted.
19
20
  #
21
+ # == Example
22
+ #
23
+ # Suppose we have a simple dispatcher. We're not going to worry about method calls, so we're just going to focus on
24
+ # implementing +._register+ and +._process_event+.
25
+ #
26
+ # class SimpleDispatcher < Emittance::Dispatcher
27
+ # class << self
28
+ # private
29
+ #
30
+ # # +._register+ takes the original identifier/topic, an optional params hash, and a block. To register the
31
+ # # callback, push it on to the collection returned by the +.registrations_for+ method. You can format the
32
+ # # callback in any way you like.
33
+ # def _register(identifier, _params = {}, &callback)
34
+ # registrations_for(event) << format_callback(callback)
35
+ # callback
36
+ # end
37
+ #
38
+ # # +._process_event+ is simple -- we just need to fetch the registrations related to the event, and process
39
+ # # them. The registrations are returned in the exact same format as they were stored in +._register+. So, in
40
+ # # this case, we will get a set of +CallbackWrapper+ objects, each of which responds to +#call+.
41
+ # #
42
+ # # IMPORTANT: You also need to make sure that the +down+ middleware stack is called in the process.
43
+ # def _process_event(event)
44
+ # event = Emittance::Middleware.down(event)
45
+ # registrations_for(event).each { |registration| registration.call(event) }
46
+ # end
47
+ #
48
+ # def format_callback(callback)
49
+ # CallbackWrapper.new(callback)
50
+ # end
51
+ # end
52
+ # end
53
+ #
20
54
  class Dispatcher
55
+ ROUTING_STRATEGIES = {
56
+ classical: RegistrationMap,
57
+ topical: TopicRegistrationMap
58
+ }.freeze
59
+
60
+ @routing_strategy = RegistrationMap
61
+
21
62
  class << self
22
- # @private
23
- def inherited(subklass)
24
- subklass.instance_variable_set '@registrations', RegistrationMap.new
63
+ def routing_strategy
64
+ @routing_strategy || ::Emittance::Dispatcher.instance_variable_get('@routing_strategy')
65
+ end
66
+
67
+ alias registration_router_klass routing_strategy
68
+
69
+ def routing_strategy=(new_strategy_name)
70
+ new_strategy =
71
+ if new_strategy_name.is_a?(Module)
72
+ new_strategy_name
73
+ else
74
+ ROUTING_STRATEGIES[new_strategy_name.to_sym]
75
+ end
76
+
77
+ raise ArgumentError, 'Could not find a routing strategy with that name' unless new_strategy
78
+
79
+ @routing_strategy = new_strategy
25
80
  end
26
81
 
27
82
  # Calls the subclass's +_process_event+ method.
@@ -47,7 +102,7 @@ module Emittance
47
102
 
48
103
  # @return [RegistrationMap] the registrations
49
104
  def clear_registrations!
50
- registrations.each_key { |key| clear_registrations_for! key }
105
+ registrations.clear
51
106
  registrations
52
107
  end
53
108
 
@@ -59,7 +114,9 @@ module Emittance
59
114
 
60
115
  private
61
116
 
62
- attr_reader :registrations
117
+ def registrations
118
+ @registrations ||= registration_router_klass.new
119
+ end
63
120
 
64
121
  def _process_event(_event)
65
122
  raise NotImplementedError
@@ -11,6 +11,7 @@ module Emittance
11
11
 
12
12
  def _process_event(event)
13
13
  registrations_for(event).each do |registration|
14
+ event = Emittance::Middleware.down(event)
14
15
  registration.call event
15
16
  end
16
17
  end
@@ -58,14 +58,12 @@ module Emittance
58
58
  # @param payload [*] any additional information that might be helpful for an event's handler to have. Can be
59
59
  # standardized on a per-event basis by pre-defining the class associated with the identifier and validating
60
60
  # the payload. See {Emittance::Event} for more details.
61
- # @param broker [Symbol] the identifier for the broker you wish to handle the event. Requires additional gems
62
- # if not using the default.
63
61
  #
64
62
  # @return the payload
65
63
  def emit(identifier, payload: nil)
66
64
  now = Time.now
67
65
  event_klass = _event_klass_for identifier
68
- event = event_klass.new(self, now, payload)
66
+ event = event_klass.new(self, now, payload).tap { |the_event| the_event.topic = identifier }
69
67
  _send_to_broker event
70
68
 
71
69
  payload
@@ -76,11 +74,10 @@ module Emittance
76
74
  #
77
75
  # @param identifiers [*] anything that can be used to generate an +Event+ class.
78
76
  # @param payload (@see #emit)
79
- # @param broker (@see #emit)
80
77
  def emit_with_dynamic_identifier(*identifiers, payload:)
81
78
  now = Time.now
82
79
  event_klass = _event_klass_for(*identifiers)
83
- event = event_klass.new(self, now, payload)
80
+ event = event_klass.new(self, now, payload).tap { |the_event| the_event.topic = identifiers.join('.') }
84
81
  _send_to_broker event
85
82
 
86
83
  payload
@@ -2,6 +2,21 @@
2
2
 
3
3
  module Emittance
4
4
  ##
5
+ # = Topical Event Lookup
6
+ #
7
+ # This section describes the new ('topical') style of event lookup. This will be maintained in the future in favor
8
+ # of the old ('classical') style. However, it is not enabled by default. To enable this strategy, you must configure
9
+ # Emittance to use it:
10
+ #
11
+ # Emittance.event_routing_strategy = :topical
12
+ #
13
+ # This strategy mimicks the topic name format used by RabbitMQ.
14
+ #
15
+ # = Classical Event Lookup (Legacy)
16
+ #
17
+ # This section describes the old ('classical') style of event lookup. While it's unlikely to be removed, it will
18
+ # remain unsupported in favor of the new ('topical') style of event lookup, described above.
19
+ #
5
20
  # Basic usage of Emittance doesn't require that you fiddle with objects of type +Emittance::Event+. However, this
6
21
  # class is open for you to inherit from in the cases where you would like to customize some aspects of the event.
7
22
  #
@@ -104,10 +119,37 @@ module Emittance
104
119
  # We can manually add an identifier post-hoc, but this would nevertheless become confusing.
105
120
  #
106
121
  class Event
122
+ LOOKUP_STRATEGIES = {
123
+ classical: EventLookup,
124
+ topical: TopicLookup
125
+ }.freeze
126
+
127
+ @lookup_strategy = EventLookup
128
+
107
129
  class << self
130
+ attr_reader :lookup_strategy
131
+
132
+ # @param new_strategy_name [#to_sym] the name of the new lookup strategy
133
+ def lookup_strategy=(new_strategy_name)
134
+ new_strategy =
135
+ if new_strategy_name.is_a?(Module)
136
+ new_strategy_name
137
+ else
138
+ LOOKUP_STRATEGIES[new_strategy_name.to_sym]
139
+ end
140
+
141
+ raise ArgumentError, 'Could not find a lookup strategy with that name' unless new_strategy
142
+
143
+ @lookup_strategy = new_strategy
144
+ end
145
+
146
+ def inherited(subklass)
147
+ subklass.instance_variable_set('@lookup_strategy', lookup_strategy)
148
+ end
149
+
108
150
  # @return [Array<Symbol>] the identifier that can be used by the {Emittance::Broker broker} to find event handlers
109
- def identifiers
110
- EventLookup.identifiers_for_klass(self).to_a
151
+ def identifiers(event = nil)
152
+ lookup_strategy.identifiers_for_klass(self, event).to_a
111
153
  end
112
154
 
113
155
  # Gives the Event object a custom identifier.
@@ -115,17 +157,19 @@ module Emittance
115
157
  # @param sym [Symbol] the identifier you wish to identify this event by when emitting and watching for it
116
158
  def add_identifier(sym)
117
159
  raise Emittance::InvalidIdentifierError, 'Identifiers must respond to #to_sym' unless sym.respond_to?(:to_sym)
118
- EventLookup.register_identifier self, sym.to_sym
160
+
161
+ lookup_strategy.register_identifier self, sym.to_sym
119
162
  end
120
163
 
121
164
  # @param identifiers [*] anything that can be derived into an identifier (or the event class itself) for the
122
165
  # purposes of looking up an event class.
123
166
  def event_klass_for(*identifiers)
124
- EventLookup.find_event_klass(*identifiers)
167
+ lookup_strategy.find_event_klass(*identifiers)
125
168
  end
126
169
  end
127
170
 
128
171
  attr_reader :emitter, :timestamp, :payload
172
+ attr_accessor :topic
129
173
 
130
174
  # @param emitter the object that emitted the event
131
175
  # @param timestamp [Time] the time at which the event occurred
@@ -138,7 +182,7 @@ module Emittance
138
182
 
139
183
  # @return [Array<Symbol>] all identifiers that can be used to identify the event
140
184
  def identifiers
141
- self.class.identifiers
185
+ self.class.identifiers(self)
142
186
  end
143
187
  end
144
188
  end
@@ -8,9 +8,9 @@ module Emittance
8
8
  # event class.
9
9
  #
10
10
  module EventLookup
11
- class << self
12
- include Emittance::Helpers::StringHelpers
11
+ extend Emittance::Helpers::StringHelpers
13
12
 
13
+ class << self
14
14
  # Look up an {Emittance::Event} class by an identifier. Generates an Event class if no such class exists for
15
15
  # that identifier.
16
16
  #
@@ -55,7 +55,7 @@ module Emittance
55
55
 
56
56
  # @param klass [Class] a subclass of {Emittance::Event} you wish to find the identifiers for
57
57
  # @return [Set<Symbol>] a collection of identifiers that can be used to identify that event class
58
- def identifiers_for_klass(klass)
58
+ def identifiers_for_klass(klass, _event = nil)
59
59
  Emittance::EventLookup::Registry.identifiers_for_klass(klass)
60
60
  end
61
61
 
@@ -219,7 +219,7 @@ module Emittance
219
219
  #
220
220
  # @param event_klass [Class] the class you want the identifiers for
221
221
  # @return [Set<Symbol>] all identifiers that can be used to identify the given event class
222
- def identifiers_for_klass(event_klass)
222
+ def identifiers_for_klass(event_klass, _event = nil)
223
223
  lookup_klass_to_identifier_mapping(event_klass) ||
224
224
  (create_mapping_for_klass(event_klass) && lookup_klass_to_identifier_mapping(event_klass))
225
225
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Emittance
6
+ class Middleware
7
+ ##
8
+ # Middleware for logging events
9
+ #
10
+ class Logging < Emittance::Middleware
11
+ @current_logger = Logger.new(STDOUT)
12
+
13
+ class << self
14
+ attr_accessor :current_logger
15
+ end
16
+
17
+ def up
18
+ current_logger.info event_log_str
19
+
20
+ event
21
+ end
22
+
23
+ private
24
+
25
+ def current_logger
26
+ self.class.current_logger
27
+ end
28
+
29
+ def event_log_str
30
+ "Emittance: #{event.identifiers.last.inspect} event emitted."
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emittance
4
+ ##
5
+ # Module for managing middlewares.
6
+ #
7
+ class Middleware
8
+ @registered_middlewares = []
9
+
10
+ class << self
11
+ attr_reader :registered_middlewares
12
+
13
+ # @param middleware [Class, Array<#up, #down>] the middleware or array of middlewares you wish to register.
14
+ # @return [Array] the updated list of registered middlewares.
15
+ def register(middleware)
16
+ middlewares = Array(middleware)
17
+
18
+ middlewares.each { |mw| registered_middlewares << mw }
19
+ end
20
+
21
+ def clear_registrations!
22
+ registered_middlewares.clear
23
+ end
24
+
25
+ def up(input_event)
26
+ registered_middlewares.reduce(input_event) { |event, klass| klass.new(event).up }
27
+ end
28
+
29
+ def down(input_event)
30
+ registered_middlewares.reduce(input_event) { |event, klass| klass.new(event).down }
31
+ end
32
+ end
33
+
34
+ attr_reader :event
35
+
36
+ def initialize(event)
37
+ @event = event
38
+ end
39
+
40
+ def up
41
+ event
42
+ end
43
+
44
+ def down
45
+ event
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emittance
4
+ ##
5
+ # Since events don't need to be dynamically generated when using topics, we essentially want to stub out all of the
6
+ # class creation and registration logic.
7
+ #
8
+ module TopicLookup
9
+ class << self
10
+ def identifiers_for_klass(_klass, event = nil)
11
+ raise ArgumentError, 'Cannot generate identifiers without an event' unless event
12
+
13
+ [event.topic]
14
+ end
15
+
16
+ def register_identifier(klass, identifier)
17
+ # no op
18
+ end
19
+
20
+ def find_event_klass(*_identifiers)
21
+ Emittance::Event
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Emittance
4
- VERSION = '1.1.0'
4
+ VERSION = '2.0.0.pre.1'
5
5
  end
@@ -11,21 +11,22 @@ module Emittance
11
11
  # @param identifier [Symbol] the event's identifier
12
12
  # @param callback_method [Symbol] one option for adding a callback--the method on the object to call when the
13
13
  # event fires
14
+ # @param params [Hash] any parameters related to the registration of a watcher
14
15
  # @param callback [Block] the other option for adding a callback--the block you wish to be executed when the event
15
16
  # fires
16
17
  # @return [Proc] the block that will run when the event fires
17
- def watch(identifier, callback_method = nil, params = {}, &callback)
18
- if callback_method
19
- _dispatcher.register_method_call identifier, self, callback_method, params
18
+ def watch(identifier, callback_method = nil, **params, &callback)
19
+ if callback
20
+ _dispatcher(params).register identifier, params, &callback
20
21
  else
21
- _dispatcher.register identifier, params, &callback
22
+ _dispatcher(params).register_method_call identifier, self, callback_method, params
22
23
  end
23
24
  end
24
25
 
25
26
  private
26
27
 
27
- def _dispatcher
28
- Emittance.dispatcher
28
+ def _dispatcher(params)
29
+ Emittance.dispatcher_for(params[:broker])
29
30
  end
30
31
  end
31
32
  end
data/lib/emittance.rb CHANGED
@@ -6,12 +6,14 @@ require 'emittance/errors'
6
6
  require 'emittance/helpers/string_helpers'
7
7
  require 'emittance/helpers/constant_helpers'
8
8
  require 'emittance/event_lookup'
9
+ require 'emittance/topic_lookup'
9
10
  require 'emittance/dispatcher'
10
11
  require 'emittance/brokerage'
11
12
  require 'emittance/broker'
12
13
  require 'emittance/event'
13
14
  require 'emittance/emitter'
14
15
  require 'emittance/watcher'
16
+ require 'emittance/middleware'
15
17
  require 'emittance/notifier'
16
18
  require 'emittance/action'
17
19
 
@@ -20,6 +22,8 @@ require 'emittance/action'
20
22
  #
21
23
  module Emittance
22
24
  class << self
25
+ attr_reader :event_routing_strategy
26
+
23
27
  # Enable eventing process-wide
24
28
  def enable!
25
29
  Emittance::Brokerage.enable!
@@ -36,20 +40,45 @@ module Emittance
36
40
  end
37
41
 
38
42
  # @return [Class] the currently enabled broker class
39
- def broker
40
- Emittance::Brokerage.broker
43
+ def default_broker
44
+ Emittance::Brokerage.default_broker
45
+ end
46
+
47
+ alias broker default_broker
48
+
49
+ def dispatcher_for(*args)
50
+ Emittance::Brokerage.dispatcher_for(*args)
41
51
  end
42
52
 
53
+ alias dispatcher dispatcher_for
54
+
43
55
  # @return [Class] the dispatcher attached to the currently enabled broker
44
- def dispatcher
45
- broker.dispatcher
56
+ def default_dispatcher
57
+ default_broker.dispatcher
46
58
  end
47
59
 
48
- # @param [identifier] the identifier that can be used to identify the broker you wish to use
60
+ # @param identifier [#to_sym] the identifier that can be used to identify the broker you wish to use
49
61
  def use_broker(identifier)
50
62
  Emittance::Brokerage.use_broker identifier
51
63
  end
52
64
 
65
+ def default_broker=(identifier)
66
+ Emittance::Brokerage.default_broker = identifier
67
+ end
68
+
69
+ # Emittance.use_middleware MyMiddleware
70
+ # Emittance.use_middleware MyOtherMiddleware, MyCoolMiddleware
71
+ #
72
+ # @param middlewares [Array<Class>] each middleware you wish to run.
73
+ def use_middleware(*middlewares)
74
+ Emittance::Middleware.register middlewares
75
+ end
76
+
77
+ # Removes all middlewares from the app.
78
+ def clear_middleware!
79
+ Emittance::Middleware.clear_registrations!
80
+ end
81
+
53
82
  # Not yet implemented! Remove nocov and private flags when finished.
54
83
  # :nocov:
55
84
  # @private
@@ -58,6 +87,12 @@ module Emittance
58
87
  # Emittance::Dispatcher.suppress(&blk)
59
88
  end
60
89
  # :nocov:
90
+
91
+ def event_routing_strategy=(new_strategy)
92
+ @event_routing_strategy = new_strategy
93
+ Emittance::Event.lookup_strategy = new_strategy
94
+ Emittance::Dispatcher.routing_strategy = new_strategy
95
+ end
61
96
  end
62
97
  end
63
98
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: emittance
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.0.0.pre.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Guillen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-01-17 00:00:00.000000000 Z
11
+ date: 2019-03-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
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'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: simplecov
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -106,6 +120,7 @@ files:
106
120
  - ".ruby-version"
107
121
  - ".travis.yml"
108
122
  - ".yardopts"
123
+ - CHANGELOG.md
109
124
  - CODE_OF_CONDUCT.md
110
125
  - Gemfile
111
126
  - Gemfile.lock
@@ -114,6 +129,7 @@ files:
114
129
  - Rakefile
115
130
  - bin/console
116
131
  - bin/setup
132
+ - docs/RoutingStrategies.md
117
133
  - emittance.gemspec
118
134
  - lib/emittance.rb
119
135
  - lib/emittance/action.rb
@@ -123,6 +139,7 @@ files:
123
139
  - lib/emittance/dispatcher.rb
124
140
  - lib/emittance/dispatcher/registration_collection_proxy.rb
125
141
  - lib/emittance/dispatcher/registration_map.rb
142
+ - lib/emittance/dispatcher/topic_registration_map.rb
126
143
  - lib/emittance/dispatchers/synchronous.rb
127
144
  - lib/emittance/emitter.rb
128
145
  - lib/emittance/errors.rb
@@ -130,7 +147,10 @@ files:
130
147
  - lib/emittance/event_lookup.rb
131
148
  - lib/emittance/helpers/constant_helpers.rb
132
149
  - lib/emittance/helpers/string_helpers.rb
150
+ - lib/emittance/middleware.rb
151
+ - lib/emittance/middleware/logging.rb
133
152
  - lib/emittance/notifier.rb
153
+ - lib/emittance/topic_lookup.rb
134
154
  - lib/emittance/version.rb
135
155
  - lib/emittance/watcher.rb
136
156
  homepage: https://github.com/aastronautss/emittance
@@ -148,12 +168,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
148
168
  version: '0'
149
169
  required_rubygems_version: !ruby/object:Gem::Requirement
150
170
  requirements:
151
- - - ">="
171
+ - - ">"
152
172
  - !ruby/object:Gem::Version
153
- version: '0'
173
+ version: 1.3.1
154
174
  requirements: []
155
175
  rubyforge_project:
156
- rubygems_version: 2.6.13
176
+ rubygems_version: 2.7.6
157
177
  signing_key:
158
178
  specification_version: 4
159
179
  summary: A robust and flexible eventing library for Ruby.