mediate 0.1.0 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 06e45660926271c9435a4c2dd1634974b3873091f12160b907833cab8c9b7abf
4
- data.tar.gz: 8b1fe7523830f57f8e2499bf8c3575ffc0eef7716478a554f683947a8958dd95
3
+ metadata.gz: 6bf88906a3d562b832c1581d5775d2f6ae2b5a7d93b59166882edec77217dde5
4
+ data.tar.gz: 0c49d3981d01e37c29e3276672e2527a2fb61a67d9f17d6d51a48a0247ebffcb
5
5
  SHA512:
6
- metadata.gz: 6f29b77858c6361cbdc54ccc79656d470dd2feb274fe341746fa66d9fc33e34a7c1906bc8df859dfbb0dbf68fa73fc5f1b7819a2bb889565c91388abfbd8d090
7
- data.tar.gz: 20c32a19443169eb74e41524b8ae9a342a023a2f54a306d182c3419988dbc5479a739c5866012d29a83e1cb1aeace714f6677efaab7c6368d90082fa0655c879
6
+ metadata.gz: efe642b92ac4763623c79af4bdc79b330544eb320e07075061ba5cec489b98cc5af62f07de4baaf34435c13ebbeedddd91611b6b137e7811bfe79d091b84d7c4
7
+ data.tar.gz: 34e033244ffcd8438060c5d6c7e05046a572b5583047b75f3438323f10e9c774d5a140066a6d7911e8da43fc01ebb52354fee7d60a9f5fbf951517aa58237d79
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mediate (0.1.0)
4
+ mediate (0.1.3)
5
5
  concurrent-ruby (~> 1.1)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Mediate
2
2
 
3
+ [![CI](https://github.com/rferg/mediate/actions/workflows/ci.yml/badge.svg)](https://github.com/rferg/mediate/actions/workflows/ci.yml) [![Gem Version](https://badge.fury.io/rb/mediate.svg)](https://badge.fury.io/rb/mediate)
4
+
3
5
  A simple mediator implementation for Ruby inspired by [Mediatr](https://github.com/jbogard/MediatR).
4
6
 
5
7
  Decouple application components by sending a request through the mediator and receiving a response from a handler, instead of directly calling methods on imported classes.
@@ -17,6 +19,8 @@ Supports request/response, notifications (i.e., events), pre- and post-request h
17
19
  - [Testing](#testing)
18
20
  - [Testing implicit request handlers](#testing-implicit-request-handlers)
19
21
  - [Using with Rails](#using-with-rails)
22
+ - [Nest request handler definitions within request classes](#nest-request-handler-definitions-within-request-classes)
23
+ - [Configure Rails to eager load other handlers in non-production environments](#configure-rails-to-eager-load-other-handlers-in-non-production-environments)
20
24
  - [Development](#development)
21
25
  - [Contributing](#contributing)
22
26
  - [License](#license)
@@ -96,7 +100,7 @@ Note that only one handler can be registered for a particular request class; att
96
100
 
97
101
  #### Implicit handler declaration
98
102
 
99
- For simple handlers, you can skip the explicit `RequestHandler` declaration above and instead pass a block to `Request.handle_with`.
103
+ For simple handlers, you can skip the explicit `RequestHandler` declaration above and instead pass a lambda to `Request.handle_with`.
100
104
 
101
105
  ```ruby
102
106
  class Ping < Mediate::Request
@@ -107,14 +111,14 @@ class Ping < Mediate::Request
107
111
  super()
108
112
  end
109
113
  # This will have the same behavior as the PingHandler declaration above.
110
- handle_with { |request| "Received: #{request.message}" }
114
+ handle_with ->(request) { "Received: #{request.message}" }
111
115
  end
112
116
 
113
117
  response = Mediate.dispatch(Ping.new('hello'))
114
118
  puts response # 'Received: hello'
115
119
  ```
116
120
 
117
- Behind the scenes, this defines a `Ping::Handler` class that calls the given block in its `handle` method. For testing purposes, you can get an instance of this handler class by calling `Mediate::Request.create_implicit_handler` (see [Testing implicit request handlers](#testing-implicit-request-handlers)).
121
+ Behind the scenes, this defines a `Ping::Handler` class that calls the given lambda in its `handle` method. For testing purposes, you can get an instance of this handler class by calling `Mediate::Request.create_implicit_handler` (see [Testing implicit request handlers](#testing-implicit-request-handlers)).
118
122
 
119
123
  #### Request polymorphism
120
124
 
@@ -129,7 +133,7 @@ Unless we registered a handler for `SubPing` explicitly.
129
133
 
130
134
  ```ruby
131
135
  class SubPing < Ping
132
- handle_with { |request| "Received from SubPing: #{request.message}" }
136
+ handle_with ->(request) { "Received from SubPing: #{request.message}" }
133
137
  end
134
138
  puts Mediate.dispatch(SubPing.new('howdy')) # 'Received from SubPing: howdy'
135
139
  ```
@@ -252,13 +256,13 @@ Special consideration is only required when testing paths that invoke methods on
252
256
 
253
257
  #### Testing implicit request handlers
254
258
 
255
- How can you test a request handler defined using `handle_with` and a block like the following?
259
+ How can you test a request handler defined using `handle_with` and a lambda like the following?
256
260
 
257
261
  ```ruby
258
262
  class ExampleRequest < Mediate::Request
259
- handle_with do |request|
263
+ handle_with lambda { |request|
260
264
  # ....
261
- end
265
+ }
262
266
  end
263
267
  ```
264
268
 
@@ -276,7 +280,51 @@ end
276
280
 
277
281
  ### Using with Rails
278
282
 
279
- TODO
283
+ Handler registrations occur within methods called from class definitions. In non-production environments, by default, Rails [lazy loads](https://guides.rubyonrails.org/autoloading_and_reloading_constants.html) constants. Therefore, if handlers are not explicitly referenced, which is typical, their class definitions will not be loaded and they will not be registered as handlers on the mediator.
284
+
285
+ There are two things that need to be done to work around this behavior:
286
+
287
+ #### Nest request handler definitions within request classes
288
+
289
+ Since requests will be explicitly referenced in, say, controllers, we can force their handler constants to load with them by nesting those definitions within the request definitions. For example:
290
+
291
+ ```ruby
292
+ class MyRequest < Mediate::Request
293
+ # ...
294
+ class MyRequestHandler < Mediate::RequestHandler
295
+ handles MyRequest
296
+
297
+ def handle(request)
298
+ # ...
299
+ end
300
+ end
301
+ end
302
+ ```
303
+
304
+ This has the added benefit of colocating a handler with its request, making it easy to find. This is therefore the recommended way to declare requests and their handlers. [Implicit handler declarations](#implicit-handler-declaration) do this automatically.
305
+
306
+ #### Configure Rails to eager load other handlers in non-production environments
307
+
308
+ For other handlers (pre- and post-request behaviors, error handlers, notification handlers), it is typically either not possible or not practical to nest their declarations within the class definitions they handle. You will have to add configuration to eager load these in environments where global eager loading is turned off. This can be accomplished by adding their file paths to `config.eager_load_paths` in the relevant environment files, like so:
309
+
310
+ ```ruby
311
+ # config/environments/development.rb
312
+
313
+ Rails.application.configure do
314
+ # ...
315
+ [
316
+ 'app/use_cases/common/**/*.rb',
317
+ 'app/use_cases/**/event_handlers/**/*.rb'
318
+ ].each do |path|
319
+ config.eager_load_paths += Dir[path]
320
+ ActiveSupport::Reloader.to_prepare do
321
+ Dir[path].each { |f| require_dependency("#{Dir.pwd}/#{f}") }
322
+ end
323
+ end
324
+ end
325
+ ```
326
+
327
+ (In the above, we're assuming, for example, that we have pre- and post-request behaviors and error handlers in `app/use_cases/common/` and notification handlers in `app/use_cases/**/event_handlers/**/`.)
280
328
 
281
329
  ## Development
282
330
 
@@ -42,16 +42,16 @@ module Mediate
42
42
  def dispatch(request)
43
43
  raise ArgumentError, "request cannot be nil" if request.nil?
44
44
 
45
- request_handler = resolve_handler(@request_handlers, request.class, REQUEST_BASE)
45
+ request_handler = resolve_handler(request_handlers, request.class, REQUEST_BASE)
46
46
  raise Errors::NoHandlerError, request.class if request_handler == NullHandler
47
47
 
48
- prerequest_handlers = collect_by_inheritance(@prerequest_behaviors, request.class, REQUEST_BASE)
49
- postrequest_handlers = collect_by_inheritance(@postrequest_behaviors, request.class, REQUEST_BASE)
50
- run_request_pipeline(request, prerequest_handlers, request_handler, postrequest_handlers)
48
+ prerequest = collect_by_inheritance(prerequest_behaviors, request.class, REQUEST_BASE)
49
+ postrequest = collect_by_inheritance(postrequest_behaviors, request.class, REQUEST_BASE)
50
+ run_request_pipeline(request, prerequest, request_handler, postrequest)
51
51
  end
52
52
 
53
53
  #
54
- # Sends a notification to all register handlers for the given notification's type.
54
+ # Sends a notification to all registered handlers for the given notification's type.
55
55
  #
56
56
  # @param [Mediate::Notification] notification
57
57
  #
@@ -60,7 +60,7 @@ module Mediate
60
60
  def publish(notification)
61
61
  raise ArgumentError, "notification cannot be nil" if notification.nil?
62
62
 
63
- handler_classes = collect_by_inheritance(@notification_handlers, notification.class, NOTIF_BASE)
63
+ handler_classes = collect_by_inheritance(notification_handlers, notification.class, NOTIF_BASE)
64
64
  handler_classes.each do |handler_class|
65
65
  handler_class.new.handle(notification)
66
66
  rescue StandardError => e
@@ -73,26 +73,31 @@ module Mediate
73
73
  def register_request_handler(handler_class, request_class)
74
74
  validate_base_class(handler_class, Mediate::RequestHandler)
75
75
  validate_base_class(request_class, REQUEST_BASE)
76
- raise_if_request_handler_exists(request_class, handler_class)
77
- @request_handlers[request_class] = handler_class
76
+ request_handlers.compute(request_class) do |old_value|
77
+ unless old_value.nil? || old_value == handler_class
78
+ raise Errors::RequestHandlerAlreadyExistsError.new(request_class, old_value, handler_class)
79
+ end
80
+
81
+ handler_class
82
+ end
78
83
  end
79
84
 
80
85
  def register_notification_handler(handler_class, notif_class)
81
86
  validate_base_class(handler_class, Mediate::NotificationHandler)
82
87
  validate_base_class(notif_class, NOTIF_BASE, allow_base: true)
83
- append_to_hash_value(@notification_handlers, notif_class, handler_class)
88
+ append_to_hash_value(notification_handlers, notif_class, handler_class)
84
89
  end
85
90
 
86
91
  def register_prerequest_behavior(behavior_class, request_class)
87
92
  validate_base_class(behavior_class, Mediate::PrerequestBehavior)
88
93
  validate_base_class(request_class, REQUEST_BASE, allow_base: true)
89
- append_to_hash_value(@prerequest_behaviors, request_class, behavior_class)
94
+ append_to_hash_value(prerequest_behaviors, request_class, behavior_class)
90
95
  end
91
96
 
92
97
  def register_postrequest_behavior(behavior_class, request_class)
93
98
  validate_base_class(behavior_class, Mediate::PostrequestBehavior)
94
99
  validate_base_class(request_class, REQUEST_BASE, allow_base: true)
95
- append_to_hash_value(@postrequest_behaviors, request_class, behavior_class)
100
+ append_to_hash_value(postrequest_behaviors, request_class, behavior_class)
96
101
  end
97
102
 
98
103
  def register_error_handler(handler_class, exception_class, dispatch_class)
@@ -119,18 +124,14 @@ module Mediate
119
124
 
120
125
  private
121
126
 
122
- def raise_if_request_handler_exists(request_class, new_handler)
123
- registered = @request_handlers.fetch(request_class, nil)
124
- return if registered.nil? || registered == new_handler
125
-
126
- raise Errors::RequestHandlerAlreadyExistsError.new(request_class, registered, new_handler)
127
- end
127
+ attr_reader :exception_handlers, :postrequest_behaviors, :prerequest_behaviors, :notification_handlers,
128
+ :request_handlers
128
129
 
129
130
  def register_error_handler_for_dispatch(handler_class, exception_class, dispatch_class, dispatch_base_class)
130
131
  validate_base_class(handler_class, Mediate::ErrorHandler)
131
132
  validate_base_class(exception_class, StandardError, allow_base: true)
132
133
  validate_base_class(dispatch_class, dispatch_base_class, allow_base: true)
133
- map = @exception_handlers.fetch_or_store(exception_class, Concurrent::Map.new)
134
+ map = exception_handlers.fetch_or_store(exception_class, Concurrent::Map.new)
134
135
  append_to_hash_value(map, dispatch_class, handler_class)
135
136
  end
136
137
 
@@ -157,7 +158,7 @@ module Mediate
157
158
  end
158
159
 
159
160
  def handle_exception(dispatched, exception, dispatch_base_class)
160
- exception_to_dispatched_maps = collect_by_inheritance(@exception_handlers, exception.class, StandardError)
161
+ exception_to_dispatched_maps = collect_by_inheritance(exception_handlers, exception.class, StandardError)
161
162
  handler_classes = exception_to_dispatched_maps.reduce(Concurrent::Set.new) do |memo, curr|
162
163
  collect_by_inheritance(curr, dispatched.class, dispatch_base_class, memo)
163
164
  end
@@ -9,27 +9,25 @@ module Mediate
9
9
  IMPLICIT_HANDLER_CLASS_NAME = "Handler"
10
10
 
11
11
  #
12
- # Registers a handler for this Request type using the given block as the handle method.
12
+ # Registers a handler for this Request type using the given lambda as the handle method.
13
13
  #
14
+ # @param [Lambda] lmbda the block that will handle the request
14
15
  # @param [Mediate::Mediator] mediator the instance to register the handler on
15
- # @param [Proc] &proc the block that will handle the request
16
16
  #
17
- # @raises [ArgumentError] if no block is given
17
+ # @raises [ArgumentError] if no lambda is given
18
+ # @raises [RequestHandlerAlreadyExistsError] if handler already defined for this class
18
19
  #
19
- # @example When a request of this type is dispatched, the handle_with block will run
20
+ # @example When a request of this type is dispatched, the handle_with lambda will run
20
21
  # class MyRequest < Mediate::Request
21
- # handle_with do |request|
22
+ # handle_with lambda do |request|
22
23
  # ## do something with request...
23
24
  # end
24
25
  # end
25
- def self.handle_with(mediator = Mediate.mediator, &proc)
26
- raise ArgumentError, "expected block to be passed to #handle_with." unless proc
26
+ def self.handle_with(lmbda, mediator = Mediate.mediator)
27
+ raise ArgumentError, "expected lambda to be passed to #handle_with." if lmbda.nil?
27
28
 
28
- if implicit_handler_defined?
29
- raise "#{name}::#{IMPLICIT_HANDLER_CLASS_NAME} is already defined. Cannot create implicit handler."
30
- end
31
-
32
- handler_class = define_handler(proc)
29
+ handler_class = define_handler(lmbda)
30
+ undefine_implicit_handler # remove any previous definition (this will do nothing if it doesn't exist)
33
31
  const_set(IMPLICIT_HANDLER_CLASS_NAME, handler_class)
34
32
  mediator.register_request_handler(handler_class, self)
35
33
  end
@@ -57,17 +55,14 @@ module Mediate
57
55
  remove_const(IMPLICIT_HANDLER_CLASS_NAME)
58
56
  end
59
57
 
60
- def self.define_handler(proc)
58
+ def self.define_handler(lmbda)
61
59
  Class.new(RequestHandler) do
62
- @@handle_proc = proc # rubocop:disable Style/ClassVars
63
- def handle(request)
64
- @@handle_proc.call(request)
65
- end
60
+ define_method(:handle, lmbda)
66
61
  end
67
62
  end
68
63
 
69
64
  def self.implicit_handler_defined?
70
- const_defined?(IMPLICIT_HANDLER_CLASS_NAME)
65
+ const_defined?(IMPLICIT_HANDLER_CLASS_NAME, false) # do not check ancestors
71
66
  end
72
67
 
73
68
  private_constant :IMPLICIT_HANDLER_CLASS_NAME
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mediate
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.3"
5
5
  end
data/lib/mediate.rb CHANGED
@@ -33,7 +33,7 @@ module Mediate
33
33
  end
34
34
 
35
35
  #
36
- # Sends a notification to all register handlers for the given notification's type.
36
+ # Sends a notification to all registered handlers for the given notification's type.
37
37
  #
38
38
  # @param [Mediate::Notification] notification
39
39
  #
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mediate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Ferguson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-08-02 00:00:00.000000000 Z
11
+ date: 2023-06-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby