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: b0ee2af5ab48feddf17d097471e7298e444b261f
4
- data.tar.gz: 7b9d4960dfbaaae80abf9f93ed8e640cb1503d01
3
+ metadata.gz: 06ccf236289151dabd66b3925bc7b9bf22cf7dd2
4
+ data.tar.gz: ce90698ff8af4dfc5c9fb930af2044b5711dc7dd
5
5
  SHA512:
6
- metadata.gz: 3c596cb4d97f385ee2827a876f3d1ced7fc180ec80a03ea174aeafceb85269c42662190d486cdd426b6cf561372d3715b5743d1eea7bbea8500d61adb658acd2
7
- data.tar.gz: b570432fd7e2db6d8fb072031d17475e7bc7579c5beaab175c5d4c752bbba7451700731ddde53f86a8761dc2d417b2a1d245e18236caf34c4de02fa26c5af9bf
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
- class Subscription < ActiveRecord::Base
48
- has_many :events,
49
- class_name: 'SubscriptionEvent',
50
- foreign_key: 'subscription_uuid',
51
- primary_key: 'uuid'
52
-
53
- validates :uuid, uniqueness: true
54
- end
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
- class SubscriptionEvent < ActiveRecord::Base
67
- include EventSourcedRecord::Event
67
+ ```ruby
68
+ class SubscriptionEvent < ActiveRecord::Base
69
+ include EventSourcedRecord::Event
68
70
 
69
- serialize :data
71
+ serialize :data
70
72
 
71
- belongs_to :subscription,
72
- foreign_key: 'subscription_uuid', primary_key: 'uuid'
73
+ belongs_to :subscription,
74
+ foreign_key: 'subscription_uuid', primary_key: 'uuid'
73
75
 
74
- event_type :creation do
75
- # attributes :user_id
76
- #
77
- # validates :user_id, presence: true
78
- end
79
- end
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
- class SubscriptionCalculator < EventSourcedRecord::Calculator
86
- events :subscription_events
88
+ ```ruby
89
+ class SubscriptionCalculator < EventSourcedRecord::Calculator
90
+ events :subscription_events
87
91
 
88
- def advance_creation(event)
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
- class SubscriptionEventObserver < ActiveRecord::Observer
99
- observe :subscription_event
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
- namespace :subscription do
117
- task :recalculate => :environment do
118
- Subscription.all.each do |subscription|
119
- SubscriptionCalculator.new(subscription).run.save!
120
- end
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
- event_type :creation do
135
- attributes :bottles_per_shipment, :bottles_purchased, :user_id
141
+ ```ruby
142
+ event_type :creation do
143
+ attributes :bottles_per_shipment, :bottles_purchased, :user_id
136
144
 
137
- validates :bottles_per_shipment, presence: true, numericality: true
138
- validates :bottles_purchased, presence: true, numericality: true
139
- validates :user_id, presence: true
140
- end
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
- event = SubscriptionEvent.creation.new(
146
- bottles_purchased: 6,
147
- user_id: current_user.id
148
- )
149
- puts "Trying to purchase #{event.bottles_purchased} bottles"
150
- event.valid? # false
151
- event.errors[:bottles_per_shipment] # ["can't be blank", "is not a number"]
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
- def advance_creation(event)
158
- @subscription.user_id = event.user_id
159
- @subscription.bottles_per_shipment = event.bottles_per_shipment
160
- @subscription.bottles_left = event.bottles_purchased
161
- end
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
- event = SubscriptionEvent.creation.new(
170
- bottles_per_shipment: 1,
171
- bottles_purchased: 6,
172
- user_id: current_user.id
173
- )
174
- event.save!
175
- subscription = Subscription.last
176
- puts "Created subscription #{subscription.id}"
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
- event_type :change_settings do
196
- attributes :bottles_per_shipment
207
+ ```ruby
208
+ event_type :change_settings do
209
+ attributes :bottles_per_shipment
197
210
 
198
- validates :bottles_per_shipment, numericality: true
199
- end
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
- def advance_change_settings(event)
204
- @subscription.bottles_per_shipment = event.bottles_per_shipment
205
- end
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
- subscription.bottles_per_shipment # 1
210
- subscription.events.change_settings.create!(
211
- subscription_uuid: subscription.uuid, bottles_per_shipment: 2
212
- )
213
- subscription.reload
214
- subscription.bottles_per_shipment # 2
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
- class SubscriptionCalculator < EventSourcedRecord::Calculator
229
- events :subscription_events, :shipments
243
+ ```ruby
244
+ class SubscriptionCalculator < EventSourcedRecord::Calculator
245
+ events :subscription_events, :shipments
230
246
 
231
- # Other methods omitted
247
+ # Other methods omitted
232
248
 
233
- def advance_shipment(shipment)
234
- @subscription.bottles_left -= shipment.num_bottles
235
- end
236
- end
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
- class SubscriptionEventObserver < ActiveRecord::Observer
241
- observe :subscription_event, :shipment
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
- calculator = SubscriptionCalculator.new(subscription)
291
- sub_at_end_of_year = calculator.run(last_event_time: Date.new(2014,12,31))
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
 
@@ -7,6 +7,7 @@ class EventSourcedRecord::Event::EventTypeConfig
7
7
 
8
8
  def initialize
9
9
  @_validators = Hash.new { |h,k| h[k] = [] }
10
+ @attributes = []
10
11
  end
11
12
 
12
13
  def attributes(*attrs)
@@ -1,3 +1,3 @@
1
1
  module EventSourcedRecord
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.1"
3
3
  end
@@ -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
@@ -84,6 +84,9 @@ class SubscriptionEvent < ActiveRecord::Base
84
84
 
85
85
  validates :bottles_per_shipment, numericality: true
86
86
  end
87
+
88
+ event_type :blank do
89
+ end
87
90
  end
88
91
 
89
92
  class Subscription < ActiveRecord::Base
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.0
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-02-23 00:00:00.000000000 Z
11
+ date: 2015-04-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel