emittance 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -36,33 +36,33 @@ module Emittance
36
36
  "_non_emitting_#{method_name}".to_sym
37
37
  end
38
38
 
39
- # @private
40
- def emitting_method_event(emitter_klass, method_name)
41
- Emittance::Event.event_klass_for(emitter_klass, method_name)
42
- end
43
-
44
39
  # @private
45
40
  def emitter_eval(klass, *args, &blk)
46
41
  if klass.respond_to? :class_eval
47
- klass.class_eval *args, &blk
42
+ klass.class_eval(*args, &blk)
48
43
  else
49
- klass.singleton_class.class_eval *args, &blk
44
+ klass.singleton_class.class_eval(*args, &blk)
50
45
  end
51
46
  end
52
47
  end
53
48
  # :nocov:
54
49
 
50
+ ##
55
51
  # Included and extended whenever {Emittance::Emitter} is extended.
52
+ #
56
53
  module ClassAndInstanceMethods
57
54
  # Emits an {Emittance::Event event object} to watchers.
58
55
  #
59
56
  # @param identifier [Symbol, Emittance::Event] either an explicit Event object or the identifier that can be
60
57
  # parsed into an Event object.
61
58
  # @param payload [*] any additional information that might be helpful for an event's handler to have. Can be
62
- # standardized on a per-event basis by pre-defining the class associated with the
59
+ # standardized on a per-event basis by pre-defining the class associated with the identifier and validating
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
63
  #
64
64
  # @return the payload
65
- def emit(identifier, payload = nil, broker: :synchronous)
65
+ def emit(identifier, payload: nil, broker: :synchronous)
66
66
  now = Time.now
67
67
  event_klass = _event_klass_for identifier
68
68
  event = event_klass.new(self, now, payload)
@@ -76,56 +76,64 @@ module Emittance
76
76
  #
77
77
  # @param identifiers [*] anything that can be used to generate an +Event+ class.
78
78
  # @param payload (@see #emit)
79
+ # @param broker (@see #emit)
79
80
  def emit_with_dynamic_identifier(*identifiers, payload:, broker: :synchronous)
80
81
  now = Time.now
81
- event_klass = _event_klass_for *identifiers
82
+ event_klass = _event_klass_for(*identifiers)
82
83
  event = event_klass.new(self, now, payload)
83
84
  _send_to_broker event, broker
84
85
 
85
86
  payload
86
87
  end
87
88
 
88
- private
89
-
90
- # @private
91
- def _event_klass_for(*identifiers)
92
- Emittance::Event.event_klass_for *identifiers
93
- end
94
-
95
- # @private
96
- def _send_to_broker(event, broker)
97
- Emittance::Brokerage.send_event event, broker
98
- end
99
-
100
- # Tells the class to emit an event when a any of the given set of methods. By default, the event classes are named
101
- # accordingly: If a +Foo+ object +emits_on+ +:bar+, then the event's class will be named +FooBarEvent+, and will
102
- # be a subclass of +Emittance::Event+.
89
+ # Tells the object to emit an event when a any of the given set of methods. By default, the event classes are
90
+ # named accordingly: If a +Foo+ object +emits_on+ +:bar+, then the event's class will be named +FooBarEvent+, and
91
+ # will be a subclass of +Emittance::Event+.
103
92
  #
104
93
  # The payload for this event will be the value returned from the method call.
105
94
  #
106
95
  # @param method_names [Symbol, String, Array<Symbol, String>] the methods whose calls emit an event
107
- def emits_on(*method_names)
96
+ def emits_on(*method_names, identifier: nil)
108
97
  method_names.each do |method_name|
109
98
  non_emitting_method = Emittance::Emitter.non_emitting_method_for method_name
110
99
 
111
- Emittance::Emitter.emitter_eval(self) do
112
- if method_defined?(non_emitting_method)
113
- warn "Already emitting on #{method_name.inspect}"
114
- return
115
- end
100
+ Emittance::Emitter.emitter_eval(self, &_method_patch_block(method_name, non_emitting_method, identifier))
101
+ end
102
+ end
103
+
104
+ private
116
105
 
117
- alias_method non_emitting_method, method_name
106
+ def _method_patch_block(method_name, non_emitting_method, identifier)
107
+ lambda do |_klass|
108
+ return if method_defined?(non_emitting_method)
118
109
 
119
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
120
- def #{method_name}(*args, &blk)
121
- return_value = #{non_emitting_method}(*args, &blk)
122
- emit_with_dynamic_identifier self.class, __method__, payload: return_value
123
- return_value
124
- end
125
- RUBY
126
- end
110
+ alias_method non_emitting_method, method_name
111
+
112
+ module_eval _method_patch_str(method_name, non_emitting_method, identifier), __FILE__, __LINE__ + 1
127
113
  end
128
114
  end
115
+
116
+ def _method_patch_str(method_name, non_emitting_method, identifier)
117
+ <<~RUBY
118
+ def #{method_name}(*args, &blk)
119
+ return_value = #{non_emitting_method}(*args, &blk)
120
+ if #{!identifier.nil? ? true : false}
121
+ emit #{!identifier.nil? ? identifier : false}, payload: return_value
122
+ else
123
+ emit_with_dynamic_identifier self.class, __method__, payload: return_value
124
+ end
125
+ return_value
126
+ end
127
+ RUBY
128
+ end
129
+
130
+ def _event_klass_for(*identifiers)
131
+ Emittance::Event.event_klass_for(*identifiers)
132
+ end
133
+
134
+ def _send_to_broker(event, broker)
135
+ Emittance::Brokerage.send_event event, broker
136
+ end
129
137
  end
130
138
  end
131
139
  end
@@ -1,12 +1,15 @@
1
- # froze_string_literal: true
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Emittance
4
4
  # Raised when an identifier (for the purposes of identifying an event class) cannot be parsed, or an event class
5
5
  # can otherwise not be found or generated.
6
6
  class InvalidIdentifierError < StandardError; end
7
7
 
8
+ # Raised when an identifier couldn't be generated from a class. Typically a validation error.
9
+ class IdentifierGenerationError < StandardError; end
10
+
8
11
  # Raised when an identifier registration is attempted, but there exists an event registered to the given identifiered.
9
- class IdentifierTakenError < StandardError; end
12
+ class IdentifierCollisionError < StandardError; end
10
13
 
11
14
  # Used when a custom event type undergoes payload validation.
12
15
  class InvalidPayloadError < StandardError; end
@@ -26,48 +26,119 @@ module Emittance
26
26
  # end
27
27
  # end
28
28
  #
29
- # == Custom Identifiers
29
+ # == Identifiers
30
+ #
31
+ # Events are identified by what we call "Identifiers." These come in the form of symbols, and can be used to identify
32
+ # specific event types.
33
+ #
34
+ # === Identifier Naming
35
+ #
36
+ # The naming convention for events and their identifiers goes like this: the name of an event class will be the
37
+ # CamelCase form of its identifier, plus the word +Event+. For example, +FooEvent+ can be identified with +:foo+.
38
+ # Thus, the events received by watchers of +:foo+ will be instances of `FooEvent`. Conversely, if you make an event
39
+ # class +BarEvent+ that inherits from +Emittance::Event+, its built-in identifier will be +:bar+. You can see what
40
+ # a +Emittance::Event+ subclass's identifier is by calling +.identifiers+ on it.
41
+ #
42
+ # class SomethingHappenedEvent < Emittance::Event
43
+ # end
44
+ #
45
+ # MyEvent.identifiers
46
+ # # => [:something_happened]
47
+ #
48
+ # MyEvent.new.identifiers
49
+ # # => [:something_happened]
50
+ #
51
+ # The namespace resultion operator (+::+) in an event's class name will translate to a +/+ in the identifier name:
52
+ #
53
+ # class Foo::BarEvent < Emittance::Event
54
+ # end
55
+ #
56
+ # Foo::BarEvent.identifiers
57
+ # #=> [:'foo/bar']
58
+ #
59
+ # === Custom Identifiers
30
60
  #
31
61
  # By default, the identifier for this event will be the snake_case form of the class name with +Event+ chopped off:
32
62
  #
33
- # FooEvent.identifier # => :foo
63
+ # FooEvent.identifiers
64
+ # # => [:foo]
34
65
  #
35
66
  # You can set a custom identifier for the event class like so:
36
67
  #
37
68
  # FooEvent.add_identifier :bar
69
+ # FooEvent.identifiers
70
+ # # => [:foo, :bar]
71
+ #
72
+ # Now, when emitters emit +:bar+, this will be the event received by watchers. +#add_identifier+ will raise an
73
+ # {Emittance::IdentifierCollisionError} if you attempt to add an identifier that has already been claimed. This
74
+ # error will also be raised if you try to add an identifier that already has an associated class. For example:
75
+ #
76
+ # class FooEvent < Emittance::Event
77
+ # end
78
+ #
79
+ # class BarEvent < Emittance::Event
80
+ # end
81
+ #
82
+ # BarEvent.add_identifier :foo
83
+ # # => Emittance::IdentifierCollisionError
84
+ #
85
+ # This error is raised because, even though we haven't explicitly add the identifier +:foo+ for +FooEvent+, Emittance
86
+ # is smart enough to know that there exists a class whose name resolves to +:foo+.
87
+ #
88
+ # It's best to use custom identifiers very sparingly. One reason for this can be illustrated like so:
89
+ #
90
+ # class FooEvent < Emittance::Event
91
+ # end
92
+ #
93
+ # FooEvent.add_identifier :bar
94
+ # FooEvent.identifiers
95
+ # # => [:foo, :bar]
96
+ #
97
+ # class BarEvent < Emittance::Event
98
+ # end
99
+ #
100
+ # BarEvent.identifiers
101
+ # # => []
38
102
  #
39
- # Now, when emitters emit +:bar+, this will be the event received by watchers.
103
+ # Since +BarEvent+'s default identifier was already reserved when it was created, it could not claim that identifier.
104
+ # We can manually add an identifier post-hoc, but this would nevertheless become confusing.
40
105
  #
41
106
  class Event
42
107
  class << self
43
- # @return [Symbol] the identifier that can be used by the {Emittance::Broker broker} to find event handlers
44
- def identifier
45
- EventBuilder.klass_to_identifier self
108
+ # @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
46
111
  end
47
112
 
48
113
  # Gives the Event object a custom identifier.
49
114
  #
50
115
  # @param sym [Symbol] the identifier you wish to identify this event by when emitting and watching for it
51
116
  def add_identifier(sym)
52
- EventBuilder.register_custom_identifier self, sym
117
+ raise Emittance::InvalidIdentifierError, 'Identifiers must respond to #to_sym' unless sym.respond_to?(:to_sym)
118
+ EventLookup.register_identifier self, sym.to_sym
53
119
  end
54
120
 
55
- # @private
121
+ # @param identifiers [*] anything that can be derived into an identifier (or the event class itself) for the
122
+ # purposes of looking up an event class.
56
123
  def event_klass_for(*identifiers)
57
- EventBuilder.objects_to_klass *identifiers
124
+ EventLookup.find_event_klass(*identifiers)
58
125
  end
59
126
  end
60
127
 
61
128
  attr_reader :emitter, :timestamp, :payload
62
129
 
130
+ # @param emitter the object that emitted the event
131
+ # @param timestamp [Time] the time at which the event occurred
132
+ # @param payload any useful data that might be of use to event watchers
63
133
  def initialize(emitter, timestamp, payload)
64
134
  @emitter = emitter
65
135
  @timestamp = timestamp
66
136
  @payload = payload
67
137
  end
68
138
 
69
- def identifier
70
- self.class.identifier
139
+ # @return [Array<Symbol>] all identifiers that can be used to identify the event
140
+ def identifiers
141
+ self.class.identifiers
71
142
  end
72
143
  end
73
144
  end
@@ -0,0 +1,315 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Emittance
6
+ ##
7
+ # For looking up event classes by their identifiers. Can perform a reverse lookup of identifiers by their associated
8
+ # event class.
9
+ #
10
+ module EventLookup
11
+ class << self
12
+ include Emittance::Helpers::StringHelpers
13
+
14
+ # Look up an {Emittance::Event} class by an identifier. Generates an Event class if no such class exists for
15
+ # that identifier.
16
+ #
17
+ # EventLookup.find_event_klass :foo
18
+ # # => FooEvent
19
+ #
20
+ # When passed subclass of {Emittance::Event}, it returns that event class.
21
+ #
22
+ # class BarEvent < Emittance::Event
23
+ # end
24
+ #
25
+ # EventLookup.find_event_klass BarEvent
26
+ # # => BarEvent
27
+ #
28
+ # Instances of an {Emittance::Event} will fetch the class of that instance.
29
+ #
30
+ # EventLookup.find_event_klass BarEvent.new(nil, nil, nil)
31
+ # # => BarEvent
32
+ #
33
+ # Can be passed multiple arguments as a composite identifier. Useful for identifying events by Class#method.
34
+ #
35
+ # # Not entirely necessary, but for illustrative purposes.
36
+ # class Baz
37
+ # def greet
38
+ # end
39
+ # end
40
+ #
41
+ # EventLookup.find_event_klass Baz, :greet
42
+ # # => BazGreetEvent
43
+ #
44
+ # @param objs [*] anything that can be used to identify an Event class
45
+ # @return [Emittance::Event] the event class identifiable by the params
46
+ def find_event_klass(*objs)
47
+ klass = nil
48
+
49
+ klass ||= pass_klass_through(*objs)
50
+ klass ||= klass_of_event(*objs)
51
+ klass ||= find_by_identifier(*objs)
52
+
53
+ klass
54
+ end
55
+
56
+ # @param klass [Class] a subclass of {Emittance::Event} you wish to find the identifiers for
57
+ # @return [Set<Symbol>] a collection of identifiers that can be used to identify that event class
58
+ def identifiers_for_klass(klass)
59
+ Emittance::EventLookup::Registry.identifiers_for_klass(klass)
60
+ end
61
+
62
+ # Registers an identifier for an Event class. After registering, that identifier can be used to identify those
63
+ # events.
64
+ #
65
+ # @param klass [Class] the class you wish to register the identifier for
66
+ # @param identifier [Symbol] identifier you want to identify the class as
67
+ # @return [Class] the class for which you've just registered an identifier
68
+ def register_identifier(klass, identifier)
69
+ Emittance::EventLookup::Registry.register_identifier klass: klass, identifier: identifier
70
+ end
71
+
72
+ private
73
+
74
+ def pass_klass_through(*objs)
75
+ objs.length == 1 && event_klass?(objs[0]) ? objs[0] : nil
76
+ end
77
+
78
+ def klass_of_event(*objs)
79
+ objs.length == 1 && event_object?(objs[0]) ? objs[0].class : nil
80
+ end
81
+
82
+ def find_by_identifier(*objs)
83
+ identifier = CompositeIdentifier.new(*objs).generate
84
+ lookup_identifier identifier
85
+ end
86
+
87
+ def event_klass?(obj)
88
+ obj.is_a?(Class) && obj < Emittance::Event
89
+ end
90
+
91
+ def event_object?(obj)
92
+ obj.is_a? Emittance::Event
93
+ end
94
+
95
+ def lookup_identifier(identifier)
96
+ Emittance::EventLookup::Registry.fetch_event_klass(identifier)
97
+ end
98
+ end
99
+
100
+ ##
101
+ # Shared behavior for things that want to convert back and forth between event classes and identifiers
102
+ #
103
+ class EventKlassConverter
104
+ include Emittance::Helpers::StringHelpers
105
+
106
+ # The thing we want to append to every event class name
107
+ KLASS_NAME_SUFFIX = 'Event'
108
+ end
109
+
110
+ ##
111
+ # Converts a collection of objects to a ready-to-go identifier.
112
+ #
113
+ class CompositeIdentifier < EventKlassConverter
114
+ def initialize(*objs)
115
+ @objs = objs
116
+ end
117
+
118
+ # Compiles the objects and generates an event class name for them.
119
+ def generate
120
+ parts = objs.map { |obj| identifier_name_for obj }
121
+ compose_identifier_parts parts
122
+ end
123
+
124
+ private
125
+
126
+ attr_reader :objs
127
+
128
+ def identifier_name_for(obj)
129
+ name_str = obj.to_s
130
+ name_str = clean_up_punctuation name_str
131
+ name_str = snake_case name_str
132
+
133
+ name_str
134
+ end
135
+
136
+ def compose_identifier_parts(parts)
137
+ parts.join('_').to_sym
138
+ end
139
+ end
140
+
141
+ ##
142
+ # Derives an event class name from an identifier.
143
+ #
144
+ class EventKlassName < EventKlassConverter
145
+ def initialize(identifier)
146
+ @identifier = identifier
147
+ end
148
+
149
+ # Generates an event class name for the given identifier.
150
+ def generate
151
+ base_name = camel_case identifier.to_s
152
+ decorate_klass_name base_name
153
+ end
154
+
155
+ private
156
+
157
+ attr_reader :identifier
158
+
159
+ def decorate_klass_name(klass_name_str)
160
+ "#{klass_name_str}#{KLASS_NAME_SUFFIX}"
161
+ end
162
+ end
163
+
164
+ ##
165
+ # Derives an identifier from the name of an event class.
166
+ #
167
+ class EventIdentifier < EventKlassConverter
168
+ def initialize(klass)
169
+ @klass = klass
170
+ validate_klass
171
+ end
172
+
173
+ # Generates an identifier name for the given event class.
174
+ def generate
175
+ camel_cased_name = undecorate_klass_name(klass.name)
176
+ snake_case(camel_cased_name).to_sym
177
+ end
178
+
179
+ private
180
+
181
+ attr_reader :klass
182
+
183
+ def validate_klass
184
+ subklass_error_msg = "#{klass.name} is not a subclass of Emittance::Event!"
185
+ raise Emittance::IdentifierGenerationError, subklass_error_msg unless klass < Emittance::Event
186
+ end
187
+
188
+ def undecorate_klass_name(klass_name)
189
+ klass_name.gsub(/#{KLASS_NAME_SUFFIX}$/, '')
190
+ end
191
+ end
192
+
193
+ ##
194
+ # Caches event-to-identifier and identifier-to-event mappings. The strategy here is to lazily store/load those
195
+ # mappings. They are created on lookup. The other option would be to add a +.inherited+ method to
196
+ # {Emittance::Event} that would make subclasses register themselves, but would cause some unwanted entanglement.
197
+ #
198
+ module Registry
199
+ @identifier_to_klass_mappings = {}
200
+ @klass_to_identifier_mappings = {}
201
+
202
+ class << self
203
+ include Emittance::Helpers::ConstantHelpers
204
+
205
+ # Finds or generates the event class associated with the identifier.
206
+ #
207
+ # @param identifier [Symbol] the identifier registered to the event class you wish to fetch
208
+ # @return [Class] the event class you wish to fetch
209
+ def fetch_event_klass(identifier)
210
+ klass = nil
211
+
212
+ klass ||= identifier_to_klass_mappings[identifier]
213
+ klass ||= derive_event_klass(identifier)
214
+
215
+ klass
216
+ end
217
+
218
+ # Retrieves all identifiers associated with the event class.
219
+ #
220
+ # @param event_klass [Class] the class you want the identifiers for
221
+ # @return [Set<Symbol>] all identifiers that can be used to identify the given event class
222
+ def identifiers_for_klass(event_klass)
223
+ lookup_klass_to_identifier_mapping(event_klass) ||
224
+ (create_mapping_for_klass(event_klass) && lookup_klass_to_identifier_mapping(event_klass))
225
+ end
226
+
227
+ # Registers the given identifier for the given event class.
228
+ #
229
+ # @param klass [Class] the event class you would like to register the identifier for
230
+ # @param identifier [Symbol] the identifier with which you want to identify the event class
231
+ # @return [Class] the event class for which you've registered the identifier
232
+ def register_identifier(klass:, identifier:)
233
+ raise Emittance::InvalidIdentifierError unless valid_identifier? identifier
234
+ raise Emittance::IdentifierCollisionError if identifier_reserved? identifier, klass
235
+
236
+ identifier_to_klass_mappings[identifier] = klass
237
+
238
+ klass_to_identifier_mappings[klass] ||= empty_collection
239
+ klass_to_identifier_mappings[klass] << identifier
240
+
241
+ klass
242
+ end
243
+
244
+ # Clears all registrations.
245
+ #
246
+ # @return [Boolean] true
247
+ def clear_registrations!
248
+ identifier_to_klass_mappings.clear
249
+ klass_to_identifier_mappings.clear
250
+ end
251
+
252
+ private
253
+
254
+ attr_reader :identifier_to_klass_mappings, :klass_to_identifier_mappings
255
+
256
+ def identifier_reserved?(identifier, klass)
257
+ klass_already_exists_for_identifier?(identifier, klass) || !!identifier_to_klass_mappings[identifier]
258
+ end
259
+
260
+ def klass_already_exists_for_identifier?(identifier, klass)
261
+ derived_klass_name = klass_name_for identifier
262
+ Object.const_defined?(derived_klass_name) && klass.name != derived_klass_name
263
+ end
264
+
265
+ def lookup_klass_to_identifier_mapping(event_klass)
266
+ klass_to_identifier_mappings[event_klass]
267
+ end
268
+
269
+ def create_mapping_for_klass(event_klass)
270
+ new_identifier = derive_identifier_from_klass(event_klass)
271
+ register_identifier(identifier: new_identifier, klass: event_klass)
272
+
273
+ new_identifier
274
+ end
275
+
276
+ def valid_identifier?(identifier)
277
+ identifier.is_a? Symbol
278
+ end
279
+
280
+ def derive_event_klass(identifier)
281
+ klass_name = klass_name_for identifier
282
+ event_klass = find_or_create_event_klass klass_name
283
+ register_identifier(identifier: identifier, klass: event_klass)
284
+
285
+ event_klass
286
+ end
287
+
288
+ def derive_identifier_from_klass(event_klass)
289
+ EventIdentifier.new(event_klass).generate
290
+ end
291
+
292
+ def klass_name_for(identifier)
293
+ EventKlassName.new(identifier).generate
294
+ end
295
+
296
+ def find_or_create_event_klass(klass_name)
297
+ lookup_event_klass(klass_name) || create_event_klass(klass_name)
298
+ end
299
+
300
+ def lookup_event_klass(klass_name)
301
+ Object.const_defined?(klass_name) ? Object.const_get(klass_name) : nil
302
+ end
303
+
304
+ def create_event_klass(klass_name)
305
+ new_klass = Class.new(Emittance::Event)
306
+ set_namespaced_constant_by_name klass_name, new_klass
307
+ end
308
+
309
+ def empty_collection
310
+ Set.new
311
+ end
312
+ end
313
+ end
314
+ end
315
+ end