lieutenant 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/.travis.yml +7 -1
- data/README.md +266 -16
- data/lib/lieutenant.rb +1 -0
- data/lib/lieutenant/command.rb +3 -1
- data/lib/lieutenant/config.rb +3 -4
- data/lib/lieutenant/event.rb +3 -1
- data/lib/lieutenant/event_bus.rb +18 -2
- data/lib/lieutenant/event_store/in_memory.rb +12 -7
- data/lib/lieutenant/message.rb +4 -2
- data/lib/lieutenant/projection.rb +25 -0
- data/lib/lieutenant/version.rb +1 -1
- data/lieutenant.gemspec +1 -0
- metadata +17 -3
- data/lib/lieutenant/event_bus/in_memory.rb +0 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c13d7331d61d26a8ec7a9c816a25b959d2d94466
|
4
|
+
data.tar.gz: f26f9cf92f4a8064078859bd488a5cb15eb3954b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7f85b854951dd7e9556186ba9a173331cbd7cef3dd45e23e4a8e52a27508ac2d2a78a67cb2279dee7fe270fd85bb552d621d4c0d3c174b5fb6f90ec901ce22fd
|
7
|
+
data.tar.gz: 1ca3adbc3fb5fdc021b00f2621d1b65b722f724f7fa7dd554a650a0cef32167a64e48d1a54adddcdb6bc26d795ffa62a506d5e920de50cd7b22e20eb3b9f3b2e
|
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -2,9 +2,15 @@
|
|
2
2
|
|
3
3
|
## **CQRS/ES Toolkit to command them all**
|
4
4
|
|
5
|
+
[![Gem Version](https://badge.fury.io/rb/lieutenant.svg)](https://badge.fury.io/rb/lieutenant)
|
6
|
+
[![Build Status](https://travis-ci.org/gabteles/lieutenant.svg?branch=master)](https://travis-ci.org/gabteles/lieutenant)
|
7
|
+
[![Coverage Status](https://coveralls.io/repos/github/gabteles/lieutenant/badge.svg?branch=master)](https://coveralls.io/github/gabteles/lieutenant?branch=master)
|
8
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/c96a6dd822547e657829/maintainability)](https://codeclimate.com/github/gabteles/lieutenant/maintainability)
|
9
|
+
|
10
|
+
|
5
11
|
Lieutenant is a toolkit that implements various of the components of Command & Query Responsability Segregation (CQRS) and Event Sourcing (ES). It means that your application can get rid of the "current" state of the entities you choose and store all the *changes* that led them to it.
|
6
12
|
|
7
|
-
This gem aims to be most independent as possible of your tecnological choices
|
13
|
+
This gem aims to be most independent as possible of your tecnological choices: it should work with Rails, Sinatra, pure Rack apps or whatever you want.
|
8
14
|
|
9
15
|
If you are not familiarized, you may check this references:
|
10
16
|
|
@@ -37,63 +43,307 @@ By now, Lieutenant offer the components listed below. With each one, there's a d
|
|
37
43
|
- [Commands](#commands)
|
38
44
|
- [Command Sender](#command-sender)
|
39
45
|
- [Command Handlers](#command-handlers)
|
40
|
-
- [Aggregate
|
46
|
+
- [Aggregate Repository](#aggregate-repository)
|
41
47
|
- [Aggregates](#aggregates)
|
42
48
|
- [Events](#events)
|
43
49
|
- [Event Store](#event-store)
|
44
50
|
- [Event Bus](#event-bus)
|
51
|
+
- [Projections](#projections)
|
52
|
+
- [Configuration](#configuration)
|
45
53
|
|
46
54
|
### Commands
|
47
55
|
|
48
|
-
|
56
|
+
Commands are the representation of the system actions. They describe a **intention** to do something (e.g. `ScheduleMeeting`, `DisableProduct`).
|
57
|
+
|
58
|
+
This classes do not need any special methods, just define attributes and validations.
|
49
59
|
|
60
|
+
To use define them, just include `Lieutenant::Command` module. It'll allow you to use [ActiveModel Validations](http://guides.rubyonrails.org/active_record_validations.html).
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class ScheduleMeeting
|
64
|
+
include Lieutenant::Command
|
65
|
+
|
66
|
+
attr_accessor :meeting_room_uuid
|
67
|
+
attr_accessor :description
|
68
|
+
attr_accessor :date_start
|
69
|
+
attr_accessor :date_end
|
70
|
+
|
71
|
+
validates :meeting_room_uuid, presence: true
|
72
|
+
validates :description, presence: true, length: { minimum: 3 }
|
73
|
+
validates :date_start, presence: true
|
74
|
+
validates :date_end, presence: true
|
75
|
+
|
76
|
+
validate do
|
77
|
+
date_start.is_a?(Time) && date_end.is_a?(Time) && date_start < date_end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
To instantiate commands you can use `.new` or helper method `.with`, that receives the parameters as a Hash:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
ScheduleMeeting.with(
|
86
|
+
meeting_room_uuid: '4bb0a8a0-9234-477d-8df4-5f10a2fb1faa',
|
87
|
+
description: 'Annual planning',
|
88
|
+
date_start: Time.mktime(2017, 12, 15, 14, 0, 0),
|
89
|
+
date_end: Time.mktime(2017, 12, 15, 18, 0, 0)
|
90
|
+
)
|
91
|
+
```
|
50
92
|
|
51
93
|
### Command Sender
|
52
94
|
|
53
|
-
|
95
|
+
Command sender is the component that receives commands and forward them to the right handlers. It also instantiate the aggregate repository's unit of work, in order to help persistence to save only the generated events in each command handling.
|
96
|
+
|
97
|
+
You can access the command sender throught Lieutenant's config:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
Lieutenant.config.command_sender
|
101
|
+
```
|
102
|
+
|
103
|
+
It dependes on all the configuration components, so be sure to config them before calling it. See [Configuration](#configuration).
|
104
|
+
|
105
|
+
Once with the Command Sender, dispatch events by using `#dispatch` (aliased as `#call`):
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
Lieutenant.config.command_sender.dispatch(command)
|
109
|
+
```
|
54
110
|
|
55
111
|
|
56
112
|
### Command Handlers
|
57
113
|
|
58
|
-
|
114
|
+
Command Handlers are orchestrators to your business logic. They will receive a command and a aggregate repository then will call the needed operations, they can load a aggregate by the identifier or add a new one to the repository.
|
59
115
|
|
116
|
+
Handlers are simply objects that respond to `#call`. You can define them as [Proc's](https://ruby-doc.org/core-2.5.0/Proc.html), for example.
|
60
117
|
|
61
|
-
|
118
|
+
Lieutenant also defines a syntax sugar to help definition of them:
|
62
119
|
|
63
|
-
|
120
|
+
```ruby
|
121
|
+
module ScheduleHandler
|
122
|
+
include Lieutenant::CommandHandler
|
123
|
+
|
124
|
+
on(ScheduleMeeting) do |repository, command|
|
125
|
+
# ...
|
126
|
+
end
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
You can also register them directly on command sender:
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
Lieutenant.config.command_sender.register(ScheduleMeeting) do |repository, command|
|
134
|
+
# ...
|
135
|
+
end
|
136
|
+
```
|
137
|
+
|
138
|
+
It's important that command handlers do not have side-effects, since the commands **can** be retried (and eventually they will). If you, for example, send an email inside your handler, it may be sent twice in case of command retry.
|
139
|
+
|
140
|
+
### Aggregate Repository
|
141
|
+
|
142
|
+
The aggregate repository is responsible to control the changes in the application state. It means that it will collect events from created or modified aggregates.
|
143
|
+
|
144
|
+
It also implements the [Unit of Work Pattern](https://martinfowler.com/eaaCatalog/unitOfWork.html) for each dispatched command, meaning that it will know what new events where created when processing the command.
|
145
|
+
|
146
|
+
You'll interact only with the Repository Unit of Work, that is the `repository` parameter that command handlers receive. It allows you to:
|
147
|
+
|
148
|
+
#### Add an aggregate:
|
149
|
+
```ruby
|
150
|
+
aggregate = MeetingRoom.new
|
151
|
+
repository.add_aggregate(aggregate)
|
152
|
+
```
|
153
|
+
|
154
|
+
#### Load an aggregate:
|
155
|
+
```ruby
|
156
|
+
meeting_room = repository.load(MeetingRoom, command.meeting_room_uuid)
|
157
|
+
```
|
64
158
|
|
65
159
|
|
66
160
|
### Aggregates
|
67
161
|
|
68
|
-
|
162
|
+
Aggregates contain your business logic, rules between multiple entities are kept by them. Aggregates are all about the transaction consistency.
|
163
|
+
|
164
|
+
To define them, include `Lieutenant::Aggregate` into your class. When defining it's initializer, you'll need to also setup the instance, calling `#setup(id)`, where `id` is the identifier of the aggregates' instance (`SecureRandom.uuid` is encouraged).
|
69
165
|
|
166
|
+
```ruby
|
167
|
+
class MeetingRoom
|
168
|
+
include Lieutenant::Aggregate
|
169
|
+
|
170
|
+
def initialize(name)
|
171
|
+
setup(SecureRandom.uuid)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
```
|
175
|
+
|
176
|
+
Aggregates' state should only be modified by events, that can be applied using `#apply`, that will instantiate the event with provided params and fire them to aggregate's internal handlers (registered with `.on`):
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
class MeetingRoom
|
180
|
+
include Lieutenant::Aggregate
|
181
|
+
|
182
|
+
def initialize(name)
|
183
|
+
setup(SecureRandom.uuid)
|
184
|
+
apply(MeetingRoomCreated, name: name)
|
185
|
+
end
|
186
|
+
|
187
|
+
on(MeetingRoomCreated) do |event|
|
188
|
+
@name = event.name
|
189
|
+
end
|
190
|
+
end
|
191
|
+
```
|
192
|
+
|
193
|
+
To allow command handlers to modify aggregates, you can define handlers that also handles your business logic or throw errors:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
class MeetingRoom
|
197
|
+
include Lieutenant::Aggregate
|
198
|
+
|
199
|
+
def initialize(name)
|
200
|
+
setup(SecureRandom.uuid)
|
201
|
+
apply(MeetingRoomCreated, name: name)
|
202
|
+
end
|
203
|
+
|
204
|
+
def schedule_meeting(description, date_start, date_end)
|
205
|
+
# Check if meeting room is available to needed dates
|
206
|
+
|
207
|
+
raise(MeetingRoomNotAvailable) unless room_available
|
208
|
+
|
209
|
+
apply(
|
210
|
+
MeetingScheduled,
|
211
|
+
description: description,
|
212
|
+
date_start: date_start,
|
213
|
+
date_end: date_end
|
214
|
+
)
|
215
|
+
end
|
216
|
+
|
217
|
+
on(MeetingRoomCreated) do |event|
|
218
|
+
@name = event.name
|
219
|
+
@meetings = []
|
220
|
+
end
|
221
|
+
|
222
|
+
on(MeetingScheduled) do |event|
|
223
|
+
# Note that we could push a PORO instead of a Hash
|
224
|
+
# (and it wouldn't be a Lieutenant::Aggregate)
|
225
|
+
@meetings.push({
|
226
|
+
description: event.description,
|
227
|
+
date_start: event.date_start,
|
228
|
+
date_end: event.date_end
|
229
|
+
})
|
230
|
+
end
|
231
|
+
end
|
232
|
+
```
|
233
|
+
|
234
|
+
For the same reason of the command handlers, aggregates should not have side-effects inside them.
|
70
235
|
|
71
236
|
### Events
|
72
237
|
|
73
|
-
|
238
|
+
Events register what happened with aggregates since they were created. They have same features as `Commands`: you can use ActiveModel Validations and instantiate them using `#with` method.
|
239
|
+
|
240
|
+
Events exposes `aggregate_id` and `sequence_number`, that are used to know to which aggregate each event belongs to and it's order into the event stream. You should not worry about them, we use them internally ;)
|
74
241
|
|
242
|
+
```ruby
|
243
|
+
class MeetingScheduled
|
244
|
+
include Lieutenant::Event
|
245
|
+
|
246
|
+
attr_accessor :description
|
247
|
+
attr_accessor :date_start
|
248
|
+
attr_accessor :date_end
|
249
|
+
# Implicity defined:
|
250
|
+
# attr_accessor :aggregate_id (Meeting room's UUID)
|
251
|
+
# attr_accessor :sequence_number
|
252
|
+
end
|
253
|
+
```
|
75
254
|
|
76
255
|
### Event Store
|
77
256
|
|
78
|
-
|
257
|
+
Event stores handles pushing and pulling events to/from the persistence. They are used by the Aggregate Repository to commit changes collected by one unit of work.
|
258
|
+
|
259
|
+
You need to set what implementation will be used, them Lieutenant will do the magic. Please refer to [Configuration](#configuration).
|
79
260
|
|
80
261
|
|
81
262
|
### Event Bus
|
82
263
|
|
83
|
-
|
264
|
+
The event bus publishes and receives messages from the aggregates updates.
|
265
|
+
|
266
|
+
You can also listen to it's events by subscribing to them:
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
Lieutenant.config.event_bus.subscribe(MeetingScheduled) do |event|
|
270
|
+
puts "Meeting scheduled on room #{event.aggregate_id}, starts at #{event.date_start.iso8601}, ends at #{event.date_end.iso8601}"
|
271
|
+
end
|
272
|
+
```
|
273
|
+
|
274
|
+
|
275
|
+
### Projections
|
276
|
+
|
277
|
+
The Projections listens to events it is interested in and updates read models
|
278
|
+
as needed. It means that they maintain the *current* state of the data. To use
|
279
|
+
it, just include `Lieutenant::Projection`.
|
84
280
|
|
281
|
+
```ruby
|
282
|
+
module MeetingRoomProjection
|
283
|
+
include Lieutenant::Projection
|
284
|
+
|
285
|
+
on(MeetingRoomCreated) do |event|
|
286
|
+
MeetingRoomRecord.create!(
|
287
|
+
uuid: event.aggregate_id,
|
288
|
+
name: event.name,
|
289
|
+
meetings: []
|
290
|
+
)
|
291
|
+
end
|
292
|
+
|
293
|
+
on(MeetingScheduled) do |event|
|
294
|
+
meeting_room = MeetingRoomRecord.find(event.aggregate_id)
|
295
|
+
meeting_room.meetings.push({
|
296
|
+
description: event.description,
|
297
|
+
date_start: event.date_start,
|
298
|
+
date_end: event.date_end
|
299
|
+
})
|
300
|
+
meeting_room.save!
|
301
|
+
end
|
302
|
+
end
|
303
|
+
```
|
304
|
+
|
305
|
+
|
306
|
+
### Configuration
|
307
|
+
|
308
|
+
Lieutenant's configuration can be modified by using an structured or block way. By default, it uses InMemory implementation of event store.
|
309
|
+
|
310
|
+
```ruby
|
311
|
+
Lieutenant.config do |configuration|
|
312
|
+
configuration.event_store(Lieutenant::EventStore::InMemory)
|
313
|
+
end
|
314
|
+
|
315
|
+
# OR
|
316
|
+
|
317
|
+
Lieutenant.config.event_store(Lieutenant::EventStore::InMemory)
|
318
|
+
```
|
319
|
+
|
320
|
+
You can also access configuration the same way:
|
321
|
+
|
322
|
+
```ruby
|
323
|
+
Lieutenant.config do |configuration|
|
324
|
+
configuration.event_bus # => Lieutenant::EventBus
|
325
|
+
configuration.event_store # => Lieutenant::EventStore::InMemory
|
326
|
+
configuration.aggregate_repository # => Lieutentant::AggregateRepository
|
327
|
+
configuration.command_sender # => Lieutenant::CommandSender
|
328
|
+
end
|
329
|
+
|
330
|
+
# OR
|
331
|
+
|
332
|
+
Lieutenant.config.event_bus # => Lieutenant::EventBus
|
333
|
+
Lieutenant.config.event_store # => Lieutenant::EventStore::InMemory
|
334
|
+
Lieutenant.config.aggregate_repository # => Lieutentant::AggregateRepository
|
335
|
+
Lieutenant.config.command_sender # => Lieutenant::CommandSender
|
336
|
+
```
|
85
337
|
|
86
338
|
## Roadmap
|
87
339
|
|
88
340
|
In order to give some directions to the development of this gem, the roadmap below presents in a large picture of the plans to the future (more or less ordered).
|
89
341
|
|
90
|
-
- Projections
|
91
|
-
- Better documentation
|
92
|
-
- Command filters
|
93
342
|
- Command retry policies
|
94
|
-
- Sagas
|
95
343
|
- More implementations of event store
|
96
|
-
-
|
344
|
+
- Sagas
|
345
|
+
- Command filters
|
346
|
+
- Better documentation
|
97
347
|
|
98
348
|
## Development
|
99
349
|
|
data/lib/lieutenant.rb
CHANGED
@@ -16,6 +16,7 @@ module Lieutenant
|
|
16
16
|
autoload :EventStore, 'lieutenant/event_store'
|
17
17
|
autoload :Exception, 'lieutenant/exception'
|
18
18
|
autoload :Message, 'lieutenant/message'
|
19
|
+
autoload :Projection, 'lieutenant/projection'
|
19
20
|
autoload :VERSION, 'lieutenant/version'
|
20
21
|
|
21
22
|
module_function
|
data/lib/lieutenant/command.rb
CHANGED
data/lib/lieutenant/config.rb
CHANGED
@@ -3,15 +3,14 @@
|
|
3
3
|
module Lieutenant
|
4
4
|
# Manages configuration
|
5
5
|
class Config
|
6
|
-
|
7
|
-
|
8
|
-
return @event_bus = implementation if implementation
|
9
|
-
@event_bus ||= EventBus::InMemory.new
|
6
|
+
def event_bus
|
7
|
+
@event_bus ||= EventBus.new
|
10
8
|
end
|
11
9
|
|
12
10
|
# :reek:BooleanParameter
|
13
11
|
def event_store(implementation = false)
|
14
12
|
return @event_store_implementation = implementation if implementation
|
13
|
+
@event_store_implementation ||= EventStore::InMemory
|
15
14
|
@event_store ||= EventStore.new(@event_store_implementation, event_bus)
|
16
15
|
end
|
17
16
|
|
data/lib/lieutenant/event.rb
CHANGED
data/lib/lieutenant/event_bus.rb
CHANGED
@@ -2,7 +2,23 @@
|
|
2
2
|
|
3
3
|
module Lieutenant
|
4
4
|
# Publishes and receives messages from the aggregates updates
|
5
|
-
|
6
|
-
|
5
|
+
class EventBus
|
6
|
+
def initialize
|
7
|
+
@handlers = Hash.new { [] }
|
8
|
+
end
|
9
|
+
|
10
|
+
def subscribe(*event_classes, &handler)
|
11
|
+
event_classes.each do |event_class|
|
12
|
+
handlers[event_class] = handlers[event_class].push(handler)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def publish(event)
|
17
|
+
handlers[event.class].each { |handler| handler.call(event) }
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :handlers
|
7
23
|
end
|
8
24
|
end
|
@@ -5,22 +5,27 @@ module Lieutenant
|
|
5
5
|
# Memory implementation of the event store. Stores events while the application is running
|
6
6
|
class InMemory
|
7
7
|
def initialize
|
8
|
-
@store =
|
8
|
+
@store = []
|
9
|
+
@index = {}
|
9
10
|
end
|
10
11
|
|
11
12
|
def persist(events)
|
12
|
-
events.each
|
13
|
+
events.each do |event|
|
14
|
+
(index[event.aggregate_id] ||= []).push(store.size)
|
15
|
+
store.push(event)
|
16
|
+
end
|
13
17
|
end
|
14
18
|
|
15
19
|
def event_stream_for(aggregate_id)
|
16
|
-
|
17
|
-
return nil unless
|
20
|
+
aggregate_stream = index[aggregate_id]
|
21
|
+
return nil unless aggregate_stream
|
22
|
+
events = aggregate_stream.lazy.map(&store.method(:[]))
|
18
23
|
Enumerator.new { |yielder| events.each(&yielder.method(:<<)) }
|
19
24
|
end
|
20
25
|
|
21
26
|
def aggregate_sequence_number(aggregate_id)
|
22
|
-
return -1 unless
|
23
|
-
store[aggregate_id].last.sequence_number
|
27
|
+
return -1 unless index.key?(aggregate_id)
|
28
|
+
store[index[aggregate_id].last].sequence_number
|
24
29
|
end
|
25
30
|
|
26
31
|
def transaction
|
@@ -30,7 +35,7 @@ module Lieutenant
|
|
30
35
|
|
31
36
|
private
|
32
37
|
|
33
|
-
attr_reader :store
|
38
|
+
attr_reader :store, :index
|
34
39
|
end
|
35
40
|
end
|
36
41
|
end
|
data/lib/lieutenant/message.rb
CHANGED
@@ -4,8 +4,10 @@ module Lieutenant
|
|
4
4
|
# Helper to define messages with validation
|
5
5
|
module Message
|
6
6
|
def self.included(base)
|
7
|
-
base.
|
8
|
-
|
7
|
+
base.instance_eval do
|
8
|
+
extend Lieutenant::Message::ClassMethods
|
9
|
+
include ActiveModel::Validations
|
10
|
+
end
|
9
11
|
end
|
10
12
|
|
11
13
|
# Define common class methods to commands
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lieutenant
|
4
|
+
# Projection helper. Allows clean syntax to subscribe to events:
|
5
|
+
#
|
6
|
+
# module FooProjection
|
7
|
+
# include Lieutenant::Projection
|
8
|
+
#
|
9
|
+
# on(CreatedBarEvent) do |event|
|
10
|
+
# # ...
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
module Projection
|
14
|
+
def self.included(base)
|
15
|
+
base.class_eval do
|
16
|
+
extend Lieutenant::Projection
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# :reek:UtilityFunction
|
21
|
+
def on(*event_classes, &block)
|
22
|
+
Lieutenant.config.event_bus.subscribe(*event_classes, &block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/lieutenant/version.rb
CHANGED
data/lieutenant.gemspec
CHANGED
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.require_paths = ['lib']
|
23
23
|
|
24
24
|
spec.add_development_dependency 'bundler'
|
25
|
+
spec.add_development_dependency 'coveralls'
|
25
26
|
spec.add_development_dependency 'pry'
|
26
27
|
spec.add_development_dependency 'rake'
|
27
28
|
spec.add_development_dependency 'reek'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lieutenant
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gabriel Teles
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-02-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: coveralls
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: pry
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -161,7 +175,6 @@ files:
|
|
161
175
|
- lib/lieutenant/config.rb
|
162
176
|
- lib/lieutenant/event.rb
|
163
177
|
- lib/lieutenant/event_bus.rb
|
164
|
-
- lib/lieutenant/event_bus/in_memory.rb
|
165
178
|
- lib/lieutenant/event_store.rb
|
166
179
|
- lib/lieutenant/event_store/in_memory.rb
|
167
180
|
- lib/lieutenant/exception.rb
|
@@ -169,6 +182,7 @@ files:
|
|
169
182
|
- lib/lieutenant/exception/concurrency_conflict.rb
|
170
183
|
- lib/lieutenant/exception/no_registered_handler.rb
|
171
184
|
- lib/lieutenant/message.rb
|
185
|
+
- lib/lieutenant/projection.rb
|
172
186
|
- lib/lieutenant/version.rb
|
173
187
|
- lieutenant.gemspec
|
174
188
|
homepage: https://github.com/gabteles/lieutenant
|
@@ -1,31 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Lieutenant
|
4
|
-
module EventBus
|
5
|
-
# Memory implementation of the event bus. Publishes and notifies on the same memory space.
|
6
|
-
class InMemory
|
7
|
-
def initialize
|
8
|
-
@handlers = Hash.new { [] }
|
9
|
-
end
|
10
|
-
|
11
|
-
def subscribe(*event_classes, &handler)
|
12
|
-
event_classes.each do |event_class|
|
13
|
-
handlers[event_class] = handlers[event_class].push(handler)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def publish(event)
|
18
|
-
block = CALL_HANDLER_WITH_EVENT[event]
|
19
|
-
handlers[:all].each(&block)
|
20
|
-
handlers[event.class].each(&block)
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
attr_reader :handlers
|
26
|
-
|
27
|
-
CALL_HANDLER_WITH_EVENT = ->(event) { ->(handler) { handler.call(event) } }
|
28
|
-
private_constant :CALL_HANDLER_WITH_EVENT
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|