event_sourced_record 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 06ccf236289151dabd66b3925bc7b9bf22cf7dd2
|
4
|
+
data.tar.gz: ce90698ff8af4dfc5c9fb930af2044b5711dc7dd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d75a707b3c1cbf8c2f54a188895d6be623119a3dd874650cb95dd86d3a659bb5e0300de769bf85994cd037f387b90a2a7c942afbcd8eb56ca9d91bbed4578500
|
7
|
+
data.tar.gz: d23d90630fa9cbbd3419dcf6f259055aabbdbcba9d58db029ddf95fae93d033972d34e210fe03a2cc5f68b97de66eab482ade464157dcd57fbff81679bf8ea76
|
data/Getting_Started.md
CHANGED
@@ -44,15 +44,16 @@ This takes the same attribute list as `rails generate model`, but generates a nu
|
|
44
44
|
|
45
45
|
This is the model that you'd create in a typical Rails application, but here you'll find that we don't do much with it directly.
|
46
46
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
47
|
+
```ruby
|
48
|
+
class Subscription < ActiveRecord::Base
|
49
|
+
has_many :events,
|
50
|
+
class_name: 'SubscriptionEvent',
|
51
|
+
foreign_key: 'subscription_uuid',
|
52
|
+
primary_key: 'uuid'
|
53
|
+
|
54
|
+
validates :uuid, uniqueness: true
|
55
|
+
end
|
56
|
+
```
|
56
57
|
`Subscription` holds data in a convenient form, but it's not responsible for changing its own state. That's the responsiblity of the calculator, using the associated events.
|
57
58
|
|
58
59
|
In Event Sourcing parlance, `Subscription` is a "projection", meaning that everything in it can be derived from data and logic that lives elsewhere.
|
@@ -63,46 +64,51 @@ You might never end up showing this model to end-users, but in fact it's the aut
|
|
63
64
|
|
64
65
|
`SubscriptionEvent` represents a timestamped event associated with a particular `Subscription`. Each event should be treated as read-only; it's meant to be written once and then never modified.
|
65
66
|
|
66
|
-
|
67
|
-
|
67
|
+
```ruby
|
68
|
+
class SubscriptionEvent < ActiveRecord::Base
|
69
|
+
include EventSourcedRecord::Event
|
68
70
|
|
69
|
-
|
71
|
+
serialize :data
|
70
72
|
|
71
|
-
|
72
|
-
|
73
|
+
belongs_to :subscription,
|
74
|
+
foreign_key: 'subscription_uuid', primary_key: 'uuid'
|
73
75
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
76
|
+
event_type :creation do
|
77
|
+
# attributes :user_id
|
78
|
+
#
|
79
|
+
# validates :user_id, presence: true
|
80
|
+
end
|
81
|
+
end
|
82
|
+
```
|
80
83
|
|
81
84
|
### SubscriptionCalculator
|
82
85
|
|
83
86
|
This service class replays the event sequence to build a Subscription record that reflects current state. You'll flesh it out by adding methods that advance the state of the Subscription for each individual type of event.
|
84
87
|
|
85
|
-
|
86
|
-
|
88
|
+
```ruby
|
89
|
+
class SubscriptionCalculator < EventSourcedRecord::Calculator
|
90
|
+
events :subscription_events
|
87
91
|
|
88
|
-
|
89
|
-
|
90
|
-
end
|
91
|
-
end
|
92
|
+
def advance_creation(event)
|
92
93
|
|
94
|
+
end
|
95
|
+
end
|
96
|
+
```
|
93
97
|
|
94
98
|
### SubscriptionEventObserver
|
95
99
|
|
96
100
|
You can run `SubscriptionCalculator` yourself whenever you like, but `SubscriptionEventObserver` takes care of a core use-case. It monitors `SubscriptionEvent` (and other event classes, as we'll see) and tells `SubscriptionCalculator` to build or rebuild the `Subscription` every time there's a new event class saved.
|
97
101
|
|
98
|
-
|
99
|
-
|
102
|
+
```ruby
|
103
|
+
class SubscriptionEventObserver < ActiveRecord::Observer
|
104
|
+
observe :subscription_event
|
105
|
+
|
106
|
+
def after_create(event)
|
107
|
+
SubscriptionCalculator.new(event).run.save!
|
108
|
+
end
|
109
|
+
end
|
110
|
+
```
|
100
111
|
|
101
|
-
def after_create(event)
|
102
|
-
SubscriptionCalculator.new(event).run.save!
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
112
|
The generator registers this observer in `config/application.rb`:
|
107
113
|
|
108
114
|
config.active_record.observers = :subscription_event_observer
|
@@ -113,14 +119,15 @@ The Rake file gives you a convenient command to rebuild every
|
|
113
119
|
`Subscription` whenever necessary. Since you'll be building
|
114
120
|
`SubscriptionCalculator` to be idempotent, this will be a fairly safe operation.
|
115
121
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
end
|
122
|
+
```ruby
|
123
|
+
namespace :subscription do
|
124
|
+
task :recalculate => :environment do
|
125
|
+
Subscription.all.each do |subscription|
|
126
|
+
SubscriptionCalculator.new(subscription).run.save!
|
122
127
|
end
|
123
|
-
|
128
|
+
end
|
129
|
+
end
|
130
|
+
```
|
124
131
|
## Creation
|
125
132
|
|
126
133
|
The generated code starts you out with a `creation` event, but we'll want to define it to get some use out of it.
|
@@ -131,50 +138,55 @@ Above, we specified that subscriptions will have `user_id`, `bottles_per_shipmen
|
|
131
138
|
|
132
139
|
Fill out the `event_type` block in `SubscriptionEvent`:
|
133
140
|
|
134
|
-
|
135
|
-
|
141
|
+
```ruby
|
142
|
+
event_type :creation do
|
143
|
+
attributes :bottles_per_shipment, :bottles_purchased, :user_id
|
136
144
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
145
|
+
validates :bottles_per_shipment, presence: true, numericality: true
|
146
|
+
validates :bottles_purchased, presence: true, numericality: true
|
147
|
+
validates :user_id, presence: true
|
148
|
+
end
|
149
|
+
```
|
142
150
|
This lets you build and save events with the attributes `bottles_per_shipment`,
|
143
151
|
`bottles_purchased`, and `user_id`, and validates those attributes -- as long as the event type is set by using the auto-generated scope:
|
144
152
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
153
|
+
```ruby
|
154
|
+
event = SubscriptionEvent.creation.new(
|
155
|
+
bottles_purchased: 6,
|
156
|
+
user_id: current_user.id
|
157
|
+
)
|
158
|
+
puts "Trying to purchase #{event.bottles_purchased} bottles"
|
159
|
+
event.valid? # false
|
160
|
+
event.errors[:bottles_per_shipment] # ["can't be blank", "is not a number"]
|
161
|
+
```
|
152
162
|
|
153
163
|
### Handle the creation in the calculator
|
154
164
|
|
155
165
|
Fill out the `advance_creation` method in `SubscriptionCalculator`:
|
156
166
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
167
|
+
```ruby
|
168
|
+
def advance_creation(event)
|
169
|
+
@subscription.user_id = event.user_id
|
170
|
+
@subscription.bottles_per_shipment = event.bottles_per_shipment
|
171
|
+
@subscription.bottles_left = event.bottles_purchased
|
172
|
+
end
|
173
|
+
```
|
163
174
|
Note that for `user_id` and `bottles_per_shipment` we simply copy the field from the event to the subscription, but in the case of `bottles_purchased`, that is translated to `Subscription#bottles_left`. This field will go up and down over time.
|
164
175
|
|
165
176
|
### Create a subscription, indirectly
|
166
177
|
|
167
178
|
Creating a subscription is a matter of creating the event itself:
|
168
179
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
180
|
+
```ruby
|
181
|
+
event = SubscriptionEvent.creation.new(
|
182
|
+
bottles_per_shipment: 1,
|
183
|
+
bottles_purchased: 6,
|
184
|
+
user_id: current_user.id
|
185
|
+
)
|
186
|
+
event.save!
|
187
|
+
subscription = Subscription.last
|
188
|
+
puts "Created subscription #{subscription.id}"
|
189
|
+
```
|
178
190
|
### What's happening here?
|
179
191
|
|
180
192
|
There's a lot going on here. If you're curious, here's what's happening under the hood:
|
@@ -192,27 +204,30 @@ This is a lot of indirection, which you don't have to understand right away. Wh
|
|
192
204
|
|
193
205
|
Occasionally, subscribers will want to change their settings. Let's say in this case we'll only allow them to change `bottles_per_shipment`. So we add a new event type in `SubscriptionEvent`:
|
194
206
|
|
195
|
-
|
196
|
-
|
207
|
+
```ruby
|
208
|
+
event_type :change_settings do
|
209
|
+
attributes :bottles_per_shipment
|
197
210
|
|
198
|
-
|
199
|
-
|
200
|
-
|
211
|
+
validates :bottles_per_shipment, numericality: true
|
212
|
+
end
|
213
|
+
```
|
201
214
|
And we add a method to handle this new event type to `SubscriptionCalculator`:
|
202
215
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
216
|
+
```ruby
|
217
|
+
def advance_change_settings(event)
|
218
|
+
@subscription.bottles_per_shipment = event.bottles_per_shipment
|
219
|
+
end
|
220
|
+
```
|
207
221
|
If we want to change the subscription from the last example from 1 bottle per shipment to 2 bottles per shipment, this looks like this:
|
208
222
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
223
|
+
```ruby
|
224
|
+
subscription.bottles_per_shipment # 1
|
225
|
+
subscription.events.change_settings.create!(
|
226
|
+
subscription_uuid: subscription.uuid, bottles_per_shipment: 2
|
227
|
+
)
|
228
|
+
subscription.reload
|
229
|
+
subscription.bottles_per_shipment # 2
|
230
|
+
```
|
216
231
|
## Shipment
|
217
232
|
|
218
233
|
We created `SubscriptionEvent` to store events about `Subscription`, but you may find you have other kinds of classes that are like events in that they are time-based and relatively immutable. Let's say that shipments of shampoo function this way in our system: As soon as they are inserted in the database they are applied against the associated subscription.
|
@@ -225,26 +240,29 @@ And let's say that everytime a `Shipment` goes out the door, we deduct `Shipment
|
|
225
240
|
|
226
241
|
In `subscription_calculator.rb` we make two changes. First, we add `:shipments` to `events`, which tells the calculator to include `Shipment` as an event that needs to be considered. Then we add an `advance_shipment` method to handle each associated `Shipment`.
|
227
242
|
|
228
|
-
|
229
|
-
|
243
|
+
```ruby
|
244
|
+
class SubscriptionCalculator < EventSourcedRecord::Calculator
|
245
|
+
events :subscription_events, :shipments
|
230
246
|
|
231
|
-
|
247
|
+
# Other methods omitted
|
232
248
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
249
|
+
def advance_shipment(shipment)
|
250
|
+
@subscription.bottles_left -= shipment.num_bottles
|
251
|
+
end
|
252
|
+
end
|
253
|
+
```
|
238
254
|
In `subscription_event_observer.rb`, we add `:shipment` to `observer`, so the observer will know to fire when we create a shipment.
|
239
255
|
|
240
|
-
|
241
|
-
|
256
|
+
```ruby
|
257
|
+
class SubscriptionEventObserver < ActiveRecord::Observer
|
258
|
+
observe :subscription_event, :shipment
|
259
|
+
|
260
|
+
def after_create(event)
|
261
|
+
SubscriptionCalculator.new(event).run.save!
|
262
|
+
end
|
263
|
+
end
|
264
|
+
```
|
242
265
|
|
243
|
-
def after_create(event)
|
244
|
-
SubscriptionCalculator.new(event).run.save!
|
245
|
-
end
|
246
|
-
end
|
247
|
-
|
248
266
|
Note that `SubscriptionEventObserver#after_create` didn't change. When a `Shipment` is created, `after_create` will treat that `Shipment` like another type of event.
|
249
267
|
|
250
268
|
## About calculators
|
@@ -286,9 +304,9 @@ Remember, `SubscriptionCalculator` is idempotent, so this process will fix all t
|
|
286
304
|
## Reporting
|
287
305
|
|
288
306
|
Because `SubscriptionCalculator` rebuilds a record in sequence, it's a piece of cake to only partially rebuild up to a certain time, which can save a lot of time in generating retrospective reports:
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
307
|
+
```ruby
|
308
|
+
calculator = SubscriptionCalculator.new(subscription)
|
309
|
+
sub_at_end_of_year = calculator.run(last_event_time: Date.new(2014,12,31))
|
310
|
+
```
|
293
311
|
Note that the returned instance is un-saved, and shouldn't be saved to `subscriptions`, but it's now a piece of cake to take those values and send them to whatever reporting system you have in place.
|
294
312
|
|
@@ -136,4 +136,9 @@ class EventSourcedRecord::EventTest < MiniTest::Unit::TestCase
|
|
136
136
|
|
137
137
|
assert_equal(Time.new(2003, 1, 24, 11, 33), event.occurred_at)
|
138
138
|
end
|
139
|
+
|
140
|
+
def test_blank_event_type_is_okay
|
141
|
+
event = SubscriptionEvent.blank.new
|
142
|
+
assert event.valid?
|
143
|
+
end
|
139
144
|
end
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: event_sourced_record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Francis Hwang
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-04-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|