medi8-rb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +330 -0
- data/lib/medi8/configuration.rb +19 -0
- data/lib/medi8/handler.rb +18 -0
- data/lib/medi8/jobs/notification_job.rb +18 -0
- data/lib/medi8/mediator.rb +29 -0
- data/lib/medi8/middleware_stack.rb +23 -0
- data/lib/medi8/notifications.rb +42 -0
- data/lib/medi8/railtie.rb +14 -0
- data/lib/medi8/registry.rb +31 -0
- data/lib/medi8/version.rb +5 -0
- data/lib/medi8.rb +50 -0
- data/sig/medi8.rbs +51 -0
- data/spec/medi8/configuration_spec.rb +15 -0
- data/spec/medi8/handler_spec.rb +18 -0
- data/spec/medi8/mediator_spec.rb +12 -0
- data/spec/medi8/middleware_stack_spec.rb +18 -0
- data/spec/medi8/notifications_spec.rb +20 -0
- data/spec/medi8/registry_spec.rb +16 -0
- data/spec/medi8_spec.rb +15 -0
- metadata +98 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d97412258c38426f13a8e52fb52454a37a38228e2d466b285b65c205f7408424
|
|
4
|
+
data.tar.gz: e5f88badb5678bfd8d5a5fdc1d27c51b249cbbf1d673f03842db241adeb91bce
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 45fa60fb8b43af3fcd1c2ef7aad298e2a8948ab5355dc0078e6205f39fbde87a14d7044e8879a1eb4ba52fb8c0fd4d4fef8b72dec549e4d99a44624846ca81c5
|
|
7
|
+
data.tar.gz: 1aadd7477bdb056b0dd8d3bff61708194dad02de6c541362dc6086f2b073a6b626c84c59693759bac6dc1c0e76cd8a1a9867d5f78c1c93e17a2472ad063db465
|
data/README.md
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# Medi8-rb
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/medi8-rb)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
Medi8 is a lightweight, idiomatic mediator pattern implementation for Ruby and Rails, inspired by MediatR (from .NET)
|
|
7
|
+
|
|
8
|
+
Medi8 is not a 1:1 Ruby analog of MediatR, but it faithfully implements its core principles in an idiomatic Ruby way.
|
|
9
|
+
|
|
10
|
+
Medi8 is compatible with pure Ruby, Sinatra, and Rails.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- Simple `send` method for commands and queries
|
|
17
|
+
- `publish` notifications to many subscribers
|
|
18
|
+
- Middleware pipeline
|
|
19
|
+
- Async notifications with ActiveJob
|
|
20
|
+
- Rails integration via `Railtie`
|
|
21
|
+
- Fully modular, no inheritance required
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Design
|
|
26
|
+
|
|
27
|
+
```plantuml
|
|
28
|
+
@startuml
|
|
29
|
+
title Medi8 Logical Flow and Event Sequence
|
|
30
|
+
|
|
31
|
+
actor "Caller (e.g. Controller)" as Caller
|
|
32
|
+
participant "Medi8" as Medi8
|
|
33
|
+
participant "Mediator" as Mediator
|
|
34
|
+
participant "RegisterUserHandler" as Handler
|
|
35
|
+
participant "Medi8.publish" as Publisher
|
|
36
|
+
participant "SendWelcomeEmailHandler" as Emailer
|
|
37
|
+
participant "TrackRegistrationHandler" as Tracker
|
|
38
|
+
|
|
39
|
+
== Command Dispatch ==
|
|
40
|
+
|
|
41
|
+
Caller -> Medi8: send(RegisterUser)
|
|
42
|
+
Medi8 -> Mediator: new(registry)\nsend(request)
|
|
43
|
+
Mediator -> Handler: call(request)
|
|
44
|
+
|
|
45
|
+
== Event Publishing ==
|
|
46
|
+
|
|
47
|
+
Handler -> Medi8: publish(UserRegistered)
|
|
48
|
+
Medi8 -> Mediator: new(registry)\npublish(event)
|
|
49
|
+
Mediator -> Publisher: NotificationDispatcher.publish(event)
|
|
50
|
+
|
|
51
|
+
== Notify Subscribers ==
|
|
52
|
+
|
|
53
|
+
Publisher -> Emailer: call(event)
|
|
54
|
+
Publisher -> Tracker: call(event)
|
|
55
|
+
|
|
56
|
+
@enduml
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
Add this line to your Gemfile:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
gem "medi8-rb"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Then bundle:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
$ bundle install
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Or install it directly:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
$ gem install medi8-rb
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Concept
|
|
84
|
+
|
|
85
|
+
Use `Medi8.send(request)` for command/query behavior, and `Medi8.publish(event)` for notifications. Handlers are registered with a simple DSL.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Configuration
|
|
90
|
+
|
|
91
|
+
Set up in an initializer:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
# config/initializers/medi8.rb
|
|
95
|
+
Medi8.configure do |config|
|
|
96
|
+
config.use AwesomeMiddleware
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Example middleware:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
class AwesomeMiddleware
|
|
104
|
+
def call(request)
|
|
105
|
+
Rails.logger.info("Processing #{request.class.name}")
|
|
106
|
+
yield
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Usage
|
|
114
|
+
|
|
115
|
+
Medi8 uses a simple request/handler model. You define a request class, register a handler using handles, and then invoke `Medi8.send(request)`.
|
|
116
|
+
|
|
117
|
+
### Create a Request
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
# app/requests/create_user.rb
|
|
121
|
+
class CreateUser
|
|
122
|
+
attr_reader :name
|
|
123
|
+
|
|
124
|
+
def initialize(name:)
|
|
125
|
+
@name = name
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Create a Handler
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
# app/handlers/create_user_handler.rb
|
|
134
|
+
class CreateUserHandler
|
|
135
|
+
include Medi8::Handler
|
|
136
|
+
|
|
137
|
+
handles CreateUser
|
|
138
|
+
|
|
139
|
+
def call(request)
|
|
140
|
+
User.create!(name: request.name)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Send
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
Medi8.send(CreateUser.new(name: "Alice"))
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Notifications
|
|
154
|
+
|
|
155
|
+
### Define an Event
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
class UserRegistered
|
|
159
|
+
attr_reader :user_id
|
|
160
|
+
|
|
161
|
+
def initialize(user_id:)
|
|
162
|
+
@user_id = user_id
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Define a Subscriber
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
class SendWelcomeEmail
|
|
171
|
+
include Medi8::NotificationHandler
|
|
172
|
+
|
|
173
|
+
subscribes_to UserRegistered, async: true
|
|
174
|
+
|
|
175
|
+
def call(event)
|
|
176
|
+
UserMailer.welcome_email(User.find(event.user_id)).deliver_later
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Publish
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
Medi8.publish(UserRegistered.new(user_id: 1))
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Advanced Usage
|
|
190
|
+
|
|
191
|
+
Medi8 is compatible with [Active CQRS](https://github.com/kiebor81/active_cqrs).
|
|
192
|
+
|
|
193
|
+
For project layout consistency, use `events` and `handlers/events` for your Medi8 classes. This will align nicely with the expected folder structures of Active CQRS. If following this advice, remember to auto-load these directories.
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
config.autoload_paths += %W[
|
|
197
|
+
#{config.root}/app/events
|
|
198
|
+
#{config.root}/app/handlers/events
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
config.eager_load_paths += %W[
|
|
202
|
+
#{config.root}/app/events
|
|
203
|
+
#{config.root}/app/handlers/events
|
|
204
|
+
]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Alternatively, namespace appropriately if you prefer nested classes.
|
|
208
|
+
|
|
209
|
+
### Mediator Pattern with CQRS
|
|
210
|
+
|
|
211
|
+
Example Active CQRS command:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
# app/commands/create_user_command.rb
|
|
215
|
+
class CreateUserCommand
|
|
216
|
+
attr_reader :name, :email
|
|
217
|
+
|
|
218
|
+
def initialize(name:, email:)
|
|
219
|
+
@name = name
|
|
220
|
+
@email = email
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Active CQRS handler with Medi8:
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
# app/handlers/commands/create_user_handler.rb
|
|
229
|
+
class CreateUserHandler
|
|
230
|
+
def call(command)
|
|
231
|
+
user = User.create!(name: command.name, email: command.email)
|
|
232
|
+
|
|
233
|
+
Medi8.publish(UserRegistered.new(user_id: user.id, email: user.email))
|
|
234
|
+
|
|
235
|
+
user
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Medi8 notification published from the handler:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
# app/events/user_registered.rb
|
|
244
|
+
class UserRegistered
|
|
245
|
+
attr_reader :user_id, :email
|
|
246
|
+
|
|
247
|
+
def initialize(user_id:, email:)
|
|
248
|
+
@user_id = user_id
|
|
249
|
+
@email = email
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Medi8 notification handler(s):
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
# app/handlers/events/send_welcome_email_handler.rb
|
|
258
|
+
class SendWelcomeEmailHandler
|
|
259
|
+
include Medi8::NotificationHandler
|
|
260
|
+
subscribes_to UserRegistered
|
|
261
|
+
|
|
262
|
+
def call(event)
|
|
263
|
+
UserMailer.welcome_email(event.email).deliver_later
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
# app/handlers/events/track_user_registration_handler.rb
|
|
270
|
+
class TrackUserRegistrationHandler
|
|
271
|
+
include Medi8::NotificationHandler
|
|
272
|
+
subscribes_to UserRegistered
|
|
273
|
+
|
|
274
|
+
def call(event)
|
|
275
|
+
Rails.logger.info("=> User registered: #{event.user_id} (#{event.email})")
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Example controller:
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
class UsersController < ApplicationController
|
|
284
|
+
def create
|
|
285
|
+
command = CreateUserCommand.new(
|
|
286
|
+
name: params[:name],
|
|
287
|
+
email: params[:email]
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
user = CQRS_COMMAND_BUS.call(command)
|
|
291
|
+
render json: user, status: :created
|
|
292
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
293
|
+
render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Testing
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
$ bundle exec rspec
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Mock E2E
|
|
307
|
+
|
|
308
|
+
Provided is an end-to-end mock flow under `mock_e2e`. Test this from the terminal.
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
ruby mock_e2e.rb
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## License
|
|
317
|
+
|
|
318
|
+
MIT License © kiebor81
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Contributing
|
|
323
|
+
|
|
324
|
+
Bug reports and pull requests welcome.
|
|
325
|
+
|
|
326
|
+
1. Fork it
|
|
327
|
+
2. Create your feature branch (`git checkout -b my-feature`)
|
|
328
|
+
3. Commit your changes (`git commit -am 'Add feature'`)
|
|
329
|
+
4. Push to the branch (`git push origin my-feature`)
|
|
330
|
+
5. Create a new Pull Request
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Medi8
|
|
4
|
+
# Configuration class for Medi8, allowing users to set up middleware and registry.
|
|
5
|
+
class Configuration
|
|
6
|
+
# Initializes a new configuration instance with an empty registry and middleware stack.
|
|
7
|
+
def initialize
|
|
8
|
+
@registry = Medi8::Registry.new
|
|
9
|
+
@middleware_stack = Medi8::MiddlewareStack.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :registry, :middleware_stack
|
|
13
|
+
|
|
14
|
+
# Adds a new middleware registry entry.
|
|
15
|
+
def use(middleware)
|
|
16
|
+
@middleware_stack.use(middleware)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Medi8
|
|
4
|
+
# The Handler module is used to define classes that can handle specific types of requests.
|
|
5
|
+
module Handler
|
|
6
|
+
# This method is called when the module is included in a class.
|
|
7
|
+
def self.included(base)
|
|
8
|
+
base.extend(ClassMethods)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# This method is used to register a handler for a specific request class.
|
|
12
|
+
module ClassMethods
|
|
13
|
+
def handles(request_class)
|
|
14
|
+
Medi8.registry.register(request_class, self)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Medi8
|
|
4
|
+
# This job is used to handle notifications in Medi8.
|
|
5
|
+
module Jobs
|
|
6
|
+
# NotificationJob is an ActiveJob that processes notifications.
|
|
7
|
+
class NotificationJob < ActiveJob::Base
|
|
8
|
+
queue_as :default
|
|
9
|
+
|
|
10
|
+
# Perform the job with the given handler class name, event hash, and event class name.
|
|
11
|
+
def perform(handler_class_name, event_hash, event_class_name)
|
|
12
|
+
handler = handler_class_name.constantize.new
|
|
13
|
+
event = event_class_name.constantize.new(**event_hash.symbolize_keys)
|
|
14
|
+
handler.call(event)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Medi8
|
|
4
|
+
# Mediator is the main entry point for handling requests and publishing events.
|
|
5
|
+
class Mediator
|
|
6
|
+
def initialize(registry)
|
|
7
|
+
@registry = registry
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Sends a request to the appropriate handler.
|
|
11
|
+
def send(request)
|
|
12
|
+
handler_class = @registry.find_handler_for(request.class)
|
|
13
|
+
raise "No handler registered for #{request.class}" unless handler_class
|
|
14
|
+
|
|
15
|
+
final = -> { handler_class.new.call(request) }
|
|
16
|
+
|
|
17
|
+
if defined?(Medi8.middleware_stack)
|
|
18
|
+
Medi8.middleware_stack.call(request, &final)
|
|
19
|
+
else
|
|
20
|
+
final.call
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Publishes an event to all registered subscribers.
|
|
25
|
+
def publish(event)
|
|
26
|
+
NotificationDispatcher.new(@registry).publish(event)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Medi8
|
|
4
|
+
# MiddlewareStack is a stack of middleware that can be used to process requests.
|
|
5
|
+
class MiddlewareStack
|
|
6
|
+
# Initializes a new MiddlewareStack.
|
|
7
|
+
def initialize
|
|
8
|
+
@middlewares = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Adds a middleware to the stack.
|
|
12
|
+
def use(middleware)
|
|
13
|
+
@middlewares << middleware
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Calls the middlewares in reverse order, passing the request and a final block.
|
|
17
|
+
def call(request, &final)
|
|
18
|
+
@middlewares.reverse.inject(final) do |next_middleware, middleware|
|
|
19
|
+
-> { middleware.new.call(request, &next_middleware) }
|
|
20
|
+
end.call
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Medi8
|
|
4
|
+
# Medi8 Notifications Module
|
|
5
|
+
module NotificationHandler
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend ClassMethods
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Registers a class as a notification handler for a specific event class.
|
|
11
|
+
module ClassMethods
|
|
12
|
+
def subscribes_to(event_class, async: false)
|
|
13
|
+
Medi8.registry.register_notification(event_class, self, async: async)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Medi8 Notification dispatcher
|
|
19
|
+
class NotificationDispatcher
|
|
20
|
+
def initialize(registry)
|
|
21
|
+
@registry = registry
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Publishes an event to all registered handlers for that event class.
|
|
25
|
+
def publish(event) # rubocop:disable Metrics/MethodLength
|
|
26
|
+
handlers = @registry.find_notification_handlers_for(event.class)
|
|
27
|
+
handlers.each do |handler_def|
|
|
28
|
+
handler_class, async = handler_def
|
|
29
|
+
|
|
30
|
+
if async
|
|
31
|
+
Medi8::Jobs::NotificationJob.perform_later(
|
|
32
|
+
handler_class.name,
|
|
33
|
+
event.instance_variables.to_h { |var| [var.to_s.delete("@"), event.instance_variable_get(var)] },
|
|
34
|
+
event.class.name
|
|
35
|
+
)
|
|
36
|
+
else
|
|
37
|
+
handler_class.new.call(event)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Medi8
|
|
4
|
+
# Railtie for integrating Medi8 with Rails
|
|
5
|
+
class Railtie < Rails::Railtie
|
|
6
|
+
initializer "medi8.load_handlers" do
|
|
7
|
+
ActiveSupport.on_load(:after_initialize) do
|
|
8
|
+
# Auto-load files like app/requests/** and app/handlers/**
|
|
9
|
+
Dir[Rails.root.join("app", "handlers", "**", "*.rb")].each { |file| require file }
|
|
10
|
+
Dir[Rails.root.join("app", "requests", "**", "*.rb")].each { |file| require file }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Medi8
|
|
4
|
+
# Registry for managing request handlers and event notifications
|
|
5
|
+
class Registry
|
|
6
|
+
def initialize
|
|
7
|
+
@handlers = {}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Singleton instance of the Registry
|
|
11
|
+
def register(request_class, handler_class)
|
|
12
|
+
@handlers[request_class] = handler_class
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Find the handler for a given request class
|
|
16
|
+
def find_handler_for(request_class)
|
|
17
|
+
@handlers[request_class]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Register a notification event and its handler
|
|
21
|
+
def register_notification(event_class, handler_class, async: false)
|
|
22
|
+
@notifications ||= Hash.new { |h, k| h[k] = [] }
|
|
23
|
+
@notifications[event_class] << [handler_class, async]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Find all handlers for a given notification event class
|
|
27
|
+
def find_notification_handlers_for(event_class)
|
|
28
|
+
(@notifications && @notifications[event_class]) || []
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/medi8.rb
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "medi8/version"
|
|
4
|
+
require_relative "medi8/registry"
|
|
5
|
+
require_relative "medi8/mediator"
|
|
6
|
+
require_relative "medi8/handler"
|
|
7
|
+
require_relative "medi8/railtie" if defined?(Rails)
|
|
8
|
+
require_relative "medi8/configuration"
|
|
9
|
+
require_relative "medi8/middleware_stack"
|
|
10
|
+
require_relative "medi8/notifications"
|
|
11
|
+
|
|
12
|
+
# Medi8 is a lightweight event-driven framework for Ruby applications.
|
|
13
|
+
module Medi8
|
|
14
|
+
class << self
|
|
15
|
+
# Configures the Medi8 framework.
|
|
16
|
+
def configure
|
|
17
|
+
yield configuration
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the current configuration for Medi8.
|
|
21
|
+
def configuration
|
|
22
|
+
@configuration ||= Medi8::Configuration.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Provides a convenient way to access the Mediator instance.
|
|
26
|
+
def send(request)
|
|
27
|
+
Mediator.new(registry).send(request)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Publishes an event to the Mediator, which will notify all registered handlers.
|
|
31
|
+
def publish(event)
|
|
32
|
+
Mediator.new(registry).publish(event)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns the registry instance used by Medi8.
|
|
36
|
+
def registry
|
|
37
|
+
configuration.registry
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns the middleware stack used by Medi8.
|
|
41
|
+
def middleware_stack
|
|
42
|
+
configuration.middleware_stack
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Adds a middleware to the Medi8 middleware stack.
|
|
46
|
+
def use(middleware)
|
|
47
|
+
middleware_stack.use(middleware)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/sig/medi8.rbs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# medi8.rbs
|
|
2
|
+
module Medi8
|
|
3
|
+
def self.configure: () { (Configuration) -> void } -> void
|
|
4
|
+
def self.registry: () -> Registry
|
|
5
|
+
def self.send: (untyped request) -> untyped
|
|
6
|
+
def self.publish: (untyped event) -> void
|
|
7
|
+
def self.middleware_stack: () -> MiddlewareStack
|
|
8
|
+
def self.use: (untyped middleware) -> void
|
|
9
|
+
|
|
10
|
+
class Configuration
|
|
11
|
+
def registry: () -> Registry
|
|
12
|
+
def middleware_stack: () -> MiddlewareStack
|
|
13
|
+
def use: (untyped middleware) -> void
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class Registry
|
|
17
|
+
def register: (untyped request_class, untyped handler_class) -> void
|
|
18
|
+
def find_handler_for: (untyped request_class) -> untyped
|
|
19
|
+
def register_notification: (untyped event_class, untyped handler_class, ?async: bool) -> void
|
|
20
|
+
def find_notification_handlers_for: (untyped event_class) -> Array[[untyped, bool]]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class MiddlewareStack
|
|
24
|
+
def use: (untyped middleware) -> void
|
|
25
|
+
def call: (untyped request, &block) -> untyped
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class Mediator
|
|
29
|
+
def initialize: (Registry registry) -> void
|
|
30
|
+
def send: (untyped request) -> untyped
|
|
31
|
+
def publish: (untyped event) -> void
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
module Handler
|
|
35
|
+
module ClassMethods
|
|
36
|
+
def handles: (untyped request_class) -> void
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
module NotificationHandler
|
|
41
|
+
module ClassMethods
|
|
42
|
+
def subscribes_to: (untyped event_class, ?async: bool) -> void
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
module Jobs
|
|
47
|
+
class NotificationJob < ::ActiveJob::Base
|
|
48
|
+
def perform: (String handler_class_name, Hash[String, untyped] event_hash, String event_class_name) -> void
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# spec/medi8/configuration_spec.rb
|
|
2
|
+
RSpec.describe Medi8::Configuration do
|
|
3
|
+
it "exposes registry and middleware stack" do
|
|
4
|
+
config = described_class.new
|
|
5
|
+
expect(config.registry).to be_a(Medi8::Registry)
|
|
6
|
+
expect(config.middleware_stack).to be_a(Medi8::MiddlewareStack)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it "accepts middleware via use" do
|
|
10
|
+
mw = Class.new
|
|
11
|
+
config = described_class.new
|
|
12
|
+
config.use(mw)
|
|
13
|
+
expect(config.middleware_stack.instance_variable_get(:@middlewares)).to include(mw)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
RSpec.describe Medi8::Handler do
|
|
2
|
+
it "registers a handler for a request class" do
|
|
3
|
+
request_class = Struct.new(:value, keyword_init: true)
|
|
4
|
+
|
|
5
|
+
handler_class = Class.new do
|
|
6
|
+
include Medi8::Handler
|
|
7
|
+
|
|
8
|
+
# The constant must be defined *inside* the test where the request_class is in scope
|
|
9
|
+
handles request_class
|
|
10
|
+
|
|
11
|
+
def call(req)
|
|
12
|
+
"Handled #{req.value}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
expect(Medi8.registry.find_handler_for(request_class)).to eq(handler_class)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# spec/medi8/mediator_spec.rb
|
|
2
|
+
RSpec.describe Medi8::Mediator do
|
|
3
|
+
let(:registry) { Medi8::Registry.new }
|
|
4
|
+
let(:request_class) { Class.new { attr_reader :val; def initialize(val:); @val = val; end } }
|
|
5
|
+
let(:handler_class) { Class.new { def call(req); "got #{req.val}"; end } }
|
|
6
|
+
|
|
7
|
+
it "dispatches a request to its handler" do
|
|
8
|
+
registry.register(request_class, handler_class)
|
|
9
|
+
result = described_class.new(registry).send(request_class.new(val: "x"))
|
|
10
|
+
expect(result).to eq("got x")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# spec/medi8/middleware_stack_spec.rb
|
|
2
|
+
RSpec.describe Medi8::MiddlewareStack do
|
|
3
|
+
let(:stack) { described_class.new }
|
|
4
|
+
|
|
5
|
+
class TestMiddleware
|
|
6
|
+
def call(req)
|
|
7
|
+
req[:trace] << :middleware
|
|
8
|
+
yield
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "wraps execution in middleware layers" do
|
|
13
|
+
req = { trace: [] }
|
|
14
|
+
stack.use TestMiddleware
|
|
15
|
+
result = stack.call(req) { req[:trace] << :handler }
|
|
16
|
+
expect(req[:trace]).to eq([:middleware, :handler])
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
RSpec.describe Medi8::NotificationHandler do
|
|
2
|
+
it "registers and executes notification handlers" do
|
|
3
|
+
event_class = Struct.new(:data, keyword_init: true)
|
|
4
|
+
|
|
5
|
+
Class.new do
|
|
6
|
+
include Medi8::NotificationHandler
|
|
7
|
+
subscribes_to event_class
|
|
8
|
+
|
|
9
|
+
def call(event)
|
|
10
|
+
event.data << :called
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
data = []
|
|
15
|
+
event = event_class.new(data: data)
|
|
16
|
+
Medi8.publish(event)
|
|
17
|
+
|
|
18
|
+
expect(data).to include(:called)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# spec/medi8/registry_spec.rb
|
|
2
|
+
RSpec.describe Medi8::Registry do
|
|
3
|
+
let(:registry) { described_class.new }
|
|
4
|
+
let(:request_class) { Class.new }
|
|
5
|
+
let(:handler_class) { Class.new }
|
|
6
|
+
|
|
7
|
+
it "registers and retrieves a handler" do
|
|
8
|
+
registry.register(request_class, handler_class)
|
|
9
|
+
expect(registry.find_handler_for(request_class)).to eq(handler_class)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "registers and retrieves notification handlers" do
|
|
13
|
+
registry.register_notification(String, handler_class)
|
|
14
|
+
expect(registry.find_notification_handlers_for(String)).to include([handler_class, false])
|
|
15
|
+
end
|
|
16
|
+
end
|
data/spec/medi8_spec.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# spec/medi8_spec.rb
|
|
2
|
+
RSpec.describe Medi8 do
|
|
3
|
+
let(:request_class) { Class.new { attr_reader :value; def initialize(value:); @value = value; end } }
|
|
4
|
+
let(:handler_class) {
|
|
5
|
+
Class.new do
|
|
6
|
+
def call(req); "handled #{req.value}"; end
|
|
7
|
+
end
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
it "registers and sends a request to a handler" do
|
|
11
|
+
Medi8.registry.register(request_class, handler_class)
|
|
12
|
+
request = request_class.new(value: "test")
|
|
13
|
+
expect(Medi8.send(request)).to eq("handled test")
|
|
14
|
+
end
|
|
15
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: medi8-rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- kiebor81
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-07-21 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rails
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '6.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '6.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rspec
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.12'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.12'
|
|
41
|
+
description: Medi8 is a lightweight, idiomatic mediator pattern implementation for
|
|
42
|
+
Ruby and Rails, inspired by MediatR (from .NET)
|
|
43
|
+
email:
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files:
|
|
47
|
+
- sig/medi8.rbs
|
|
48
|
+
files:
|
|
49
|
+
- README.md
|
|
50
|
+
- lib/medi8.rb
|
|
51
|
+
- lib/medi8/configuration.rb
|
|
52
|
+
- lib/medi8/handler.rb
|
|
53
|
+
- lib/medi8/jobs/notification_job.rb
|
|
54
|
+
- lib/medi8/mediator.rb
|
|
55
|
+
- lib/medi8/middleware_stack.rb
|
|
56
|
+
- lib/medi8/notifications.rb
|
|
57
|
+
- lib/medi8/railtie.rb
|
|
58
|
+
- lib/medi8/registry.rb
|
|
59
|
+
- lib/medi8/version.rb
|
|
60
|
+
- sig/medi8.rbs
|
|
61
|
+
- spec/medi8/configuration_spec.rb
|
|
62
|
+
- spec/medi8/handler_spec.rb
|
|
63
|
+
- spec/medi8/mediator_spec.rb
|
|
64
|
+
- spec/medi8/middleware_stack_spec.rb
|
|
65
|
+
- spec/medi8/notifications_spec.rb
|
|
66
|
+
- spec/medi8/registry_spec.rb
|
|
67
|
+
- spec/medi8_spec.rb
|
|
68
|
+
homepage: https://github.com/kiebor81/medi8-rb
|
|
69
|
+
licenses:
|
|
70
|
+
- MIT
|
|
71
|
+
metadata: {}
|
|
72
|
+
post_install_message:
|
|
73
|
+
rdoc_options: []
|
|
74
|
+
require_paths:
|
|
75
|
+
- lib
|
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '3.0'
|
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '0'
|
|
86
|
+
requirements: []
|
|
87
|
+
rubygems_version: 3.5.17
|
|
88
|
+
signing_key:
|
|
89
|
+
specification_version: 4
|
|
90
|
+
summary: Lightweight Ruby/Rails mediator inspired by MediatR
|
|
91
|
+
test_files:
|
|
92
|
+
- spec/medi8/configuration_spec.rb
|
|
93
|
+
- spec/medi8/handler_spec.rb
|
|
94
|
+
- spec/medi8/mediator_spec.rb
|
|
95
|
+
- spec/medi8/middleware_stack_spec.rb
|
|
96
|
+
- spec/medi8/notifications_spec.rb
|
|
97
|
+
- spec/medi8/registry_spec.rb
|
|
98
|
+
- spec/medi8_spec.rb
|