event_sourced_record 0.2.0 → 0.2.1
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
|
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
|