emittance 0.0.2 → 0.0.3

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