ruby_cqrs 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a0bff53f5c549da4478fd01b6c2cbfcf37f2c113
4
+ data.tar.gz: 6b699a6579e93f27d6af1f0305a90f941bddf1d9
5
+ SHA512:
6
+ metadata.gz: 54d999384f32a945152bf47b482562212a660bb824f0dac7c0495b5d7461adac974919931ec37ad73a8d18a01e619f9097318e23a5898336ef598c119aebd8b6
7
+ data.tar.gz: 885545a8f2118cee2aa1957967b011225a6360607bd5669307c260d06baf05c7eccbc640fb288521ec39f39df7cba7fc0b01fd993f2bca3304caa3029ee4fb4a
data/License.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Raven Chen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,33 @@
1
+ [![Build Status](https://travis-ci.org/iravench/ruby_cqrs.svg?branch=master)](https://travis-ci.org/iravench/ruby_cqrs) [![Code Climate](https://codeclimate.com/github/iravench/ruby_cqrs/badges/gpa.svg)](https://codeclimate.com/github/iravench/ruby_cqrs) [![Test Coverage](https://codeclimate.com/github/iravench/ruby_cqrs/badges/coverage.svg)](https://codeclimate.com/github/iravench/ruby_cqrs)
2
+
3
+ # RubyCqrs
4
+
5
+ A Ruby implementation of CQRS, using event sourcing.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'ruby_cqrs'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install ruby_cqrs
22
+
23
+ ## Usage
24
+ refer to bellow spec for how to use this gem in your code
25
+ https://github.com/iravench/ruby_cqrs/blob/master/spec/feature/basic_usage_spec.rb
26
+
27
+ ## Contributing
28
+
29
+ 1. Fork it ( https://github.com/iravench/ruby_cqrs/fork )
30
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
31
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
32
+ 4. Push to the branch (`git push origin my-new-feature`)
33
+ 5. Create a new Pull Request
@@ -0,0 +1,13 @@
1
+ module RubyCqrs
2
+ module Data
3
+ module EventStore
4
+ def load_by guid, command_context
5
+ raise NotImplementedError
6
+ end
7
+
8
+ def save changes, command_context
9
+ raise NotImplementedError
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,70 @@
1
+ module RubyCqrs
2
+ module Data
3
+ class InMemoryEventStore
4
+ include EventStore
5
+
6
+ def initialize
7
+ @aggregate_store = {}
8
+ @event_store = {}
9
+ @snapshot_store = {}
10
+ end
11
+
12
+ def load_by guid, command_context
13
+ key = guid.to_sym
14
+ state = { :aggregate_id => guid,
15
+ :aggregate_type => @aggregate_store[key][:type] }
16
+
17
+ if @snapshot_store.has_key? key
18
+ extract_snapshot_into key, state
19
+ else
20
+ state[:events] = @event_store[key][:events]
21
+ end
22
+
23
+ state
24
+ end
25
+
26
+ def save changes, command_context
27
+ changes.each do |change|
28
+ key = change[:aggregate_id].to_sym
29
+ verify_state key, change
30
+ end
31
+ changes.each do |change|
32
+ key = change[:aggregate_id].to_sym
33
+ create_state key, change
34
+ update_state key, change
35
+ end
36
+ nil
37
+ end
38
+
39
+ private
40
+ def create_state key, change
41
+ unless @aggregate_store.has_key? key
42
+ @aggregate_store[key] = { :type => change[:aggregate_type], :version => 0 }
43
+ @event_store[key] = { :events => [] }
44
+ end
45
+ unless @snapshot_store.has_key? key or change[:snapshot].nil?
46
+ @snapshot_store[key] = {}
47
+ end
48
+ end
49
+
50
+ def update_state key, change
51
+ @aggregate_store[key][:version] = change[:expecting_version]
52
+ change[:events].each { |event| @event_store[key][:events] << event }
53
+ @snapshot_store[key] = change[:snapshot] unless change[:snapshot].nil?
54
+ end
55
+
56
+ def extract_snapshot_into key, state
57
+ snapshot_version = @snapshot_store[key][:version]
58
+ state[:events] = @event_store[key][:events]\
59
+ .select { |event_record| event_record[:version] > snapshot_version }
60
+ state[:snapshot] = @snapshot_store[key]
61
+ end
62
+
63
+ def verify_state key, change
64
+ raise AggregateConcurrencyError.new("on aggregate #{key}")\
65
+ if @aggregate_store.has_key? key\
66
+ and @aggregate_store[key][:version] != change[:expecting_source_version]
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,30 @@
1
+ require 'active_support/inflector'
2
+ require 'beefcake'
3
+
4
+ module RubyCqrs
5
+ class ObjectNotEncodableError < Error; end
6
+ class ObjectNotDecodableError < Error; end
7
+
8
+ module Data
9
+ module Encodable
10
+
11
+ def try_encode
12
+ return self.encode.to_s if self.is_a? Beefcake::Message
13
+ raise ObjectNotEncodableError
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ module RubyCqrs
20
+ module Data
21
+ module Decodable
22
+
23
+ def try_decode type_str, data
24
+ obj_type = type_str.constantize
25
+ raise ObjectNotDecodableError unless obj_type.include? Beefcake::Message
26
+ obj_type.decode data
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,107 @@
1
+ require 'active_support/inflector'
2
+
3
+ module RubyCqrs
4
+ module Domain
5
+ module Aggregate
6
+ attr_reader :aggregate_id, :version
7
+
8
+ def initialize
9
+ @aggregate_id = Guid.create
10
+ @version = 0
11
+ @source_version = 0
12
+ @event_handler_cache = {}
13
+ @pending_events = []
14
+ super
15
+ end
16
+
17
+ def is_version_conflicted? client_side_version
18
+ client_side_version != @source_version
19
+ end
20
+
21
+ private
22
+
23
+ def load_from state
24
+ sorted_events = state[:events].sort { |x, y| x.version <=> y.version }
25
+ @aggregate_id = state[:aggregate_id]
26
+ try_load_snapshot_from state
27
+ sorted_events.each do |event|
28
+ apply(event)
29
+ @source_version += 1
30
+ end
31
+ end
32
+
33
+ def try_load_snapshot_from state
34
+ if state.has_key? :snapshot and self.is_a? Snapshotable
35
+ self.send :apply_snapshot, state[:snapshot][:state]
36
+ @version = state[:snapshot][:version]
37
+ @source_version = state[:snapshot][:version]
38
+ self.send(:reset_countdown, state[:events].size)
39
+ end
40
+ end
41
+
42
+ def get_changes
43
+ return nil unless @pending_events.size > 0
44
+ changes = {
45
+ :events => @pending_events,
46
+ :aggregate_id => @aggregate_id,
47
+ :aggregate_type => self.class.name,
48
+ :expecting_source_version => @source_version,
49
+ :expecting_version => @pending_events.max\
50
+ { |a, b| a.version <=> b.version }.version
51
+ }
52
+ try_extract_snapshot_into changes
53
+ changes
54
+ end
55
+
56
+ def try_extract_snapshot_into changes
57
+ snapshot_state = self.send :take_a_snapshot\
58
+ if self.is_a? Snapshotable and self.send(:should_take_a_snapshot?)
59
+ unless snapshot_state.nil?
60
+ raise NotADomainSnapshotError unless snapshot_state.is_a? Snapshot
61
+ changes[:snapshot] = { :state => snapshot_state,
62
+ :state_type => snapshot_state.class.name,
63
+ :version => @version }
64
+ self.send :set_snapshot_taken
65
+ end
66
+ end
67
+
68
+ def commit
69
+ @pending_events = []
70
+ @source_version = @version
71
+ if self.is_a? Snapshotable and self.send :should_reset_snapshot_countdown?
72
+ self.send(:reset_countdown, 0)
73
+ end
74
+ end
75
+
76
+ def raise_event(event)
77
+ raise NotADomainEventError unless event.is_a? Event
78
+ apply(event)
79
+ update_dispatch_detail_for(event)
80
+ @pending_events << event
81
+ end
82
+
83
+ def update_dispatch_detail_for(event)
84
+ event.instance_variable_set(:@aggregate_id, @aggregate_id)
85
+ event.instance_variable_set(:@version, @version)
86
+ end
87
+
88
+ def apply(event)
89
+ dispatch_handler_for(event)
90
+ self.send(:snapshot_countdown) if self.is_a? Snapshotable
91
+ @version += 1
92
+ end
93
+
94
+ def dispatch_handler_for(event)
95
+ target = retrieve_handler_for(event.class)
96
+ self.send(target, event)
97
+ end
98
+
99
+ def retrieve_handler_for(event_type)
100
+ @event_handler_cache[event_type] ||= begin
101
+ stripped_event_type_name = event_type.to_s.demodulize.underscore
102
+ "on_#{stripped_event_type_name}".to_sym
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,122 @@
1
+ require 'active_support/inflector'
2
+ require_relative '../guid'
3
+
4
+ module RubyCqrs
5
+ class AggregateNotFound < Error; end
6
+ class AggregateConcurrencyError < Error; end
7
+ class AggregateInstanceDuplicatedError < Error; end
8
+
9
+ module Domain
10
+ class AggregateRepository
11
+ include RubyCqrs::Data::Decodable
12
+
13
+ def find_by aggregate_id
14
+ raise ArgumentError if aggregate_id.nil?
15
+ raise ArgumentError unless Guid.validate? aggregate_id
16
+
17
+ state = @event_store.load_by(aggregate_id, @command_context)
18
+ raise AggregateNotFound if (state.nil? or state[:aggregate_type].nil? or\
19
+ ((state[:events].nil? or state[:events].empty?) and state[:snapshot].nil?))
20
+
21
+ create_instance_from state
22
+ end
23
+
24
+ def save one_or_many_aggregate
25
+ raise ArgumentError if one_or_many_aggregate.nil?
26
+ return delegate_persistence_of [ one_or_many_aggregate ] if one_or_many_aggregate.is_a? Aggregate
27
+
28
+ raise ArgumentError unless one_or_many_aggregate.is_a? Enumerable and one_or_many_aggregate.size > 0
29
+ delegate_persistence_of one_or_many_aggregate
30
+ end
31
+
32
+ private
33
+ def initialize event_store, command_context
34
+ raise ArgumentError unless event_store.is_a? Data::EventStore
35
+ @event_store = event_store
36
+ @command_context = command_context
37
+ end
38
+
39
+ def create_instance_from state
40
+ try_decode_from state
41
+ instance = state[:aggregate_type].constantize.new
42
+ instance.send(:load_from, state)
43
+ instance
44
+ end
45
+
46
+ def delegate_persistence_of aggregates
47
+ verify_uniqueness_of aggregates
48
+
49
+ changes = prep_changes_for(aggregates)
50
+ if changes.size > 0
51
+ @event_store.save changes, @command_context
52
+ aggregates.each do |aggregate|
53
+ aggregate.send(:commit)
54
+ end
55
+ end
56
+
57
+ nil
58
+ end
59
+
60
+ def verify_uniqueness_of aggregates
61
+ uniq_array = aggregates.uniq { |aggregate| aggregate.aggregate_id }
62
+ raise AggregateInstanceDuplicatedError unless uniq_array.size == aggregates.size
63
+ end
64
+
65
+ def prep_changes_for aggregates
66
+ to_return = []
67
+ aggregates.inject(to_return) do |product, aggregate|
68
+ raise ArgumentError unless aggregate.is_a? Aggregate
69
+ aggregate_change = aggregate.send(:get_changes)
70
+ next if aggregate_change.nil?
71
+ try_encode_from aggregate_change
72
+ product << aggregate_change
73
+ end
74
+ to_return
75
+ end
76
+
77
+ def try_decode_from state
78
+ state[:snapshot] = decode_snapshot_state_from state[:snapshot]\
79
+ if state.has_key? :snapshot
80
+
81
+ state[:events].map! { |event_record| decode_event_from event_record }\
82
+ if state[:events].size > 0
83
+ end
84
+
85
+ def decode_snapshot_state_from snapshot_record
86
+ snapshot_state = try_decode snapshot_record[:state_type], snapshot_record[:data]
87
+ { :state => snapshot_state, :version => snapshot_record[:version] }
88
+ end
89
+
90
+ def decode_event_from event_record
91
+ decoded_event = try_decode event_record[:event_type], event_record[:data]
92
+ decoded_event.instance_variable_set(:@aggregate_id, event_record[:aggregate_id])
93
+ decoded_event.instance_variable_set(:@version, event_record[:version])
94
+ decoded_event
95
+ end
96
+
97
+ def try_encode_from change
98
+ if change.has_key? :snapshot
99
+ encoded_snapshot = encode_data_from change[:snapshot][:state]
100
+ change[:snapshot] = { :state_type => change[:snapshot][:state_type],
101
+ :version => change[:snapshot][:version],
102
+ :data => encoded_snapshot }
103
+ end
104
+
105
+ if change[:events].size > 0
106
+ change[:events].map! { |event|
107
+ { :data => encode_data_from(event),
108
+ :aggregate_id => event.aggregate_id,
109
+ :event_type => event.class.name,
110
+ :version => event.version }
111
+ }
112
+ end
113
+ end
114
+
115
+ def encode_data_from obj
116
+ data = obj
117
+ data = data.try_encode if data.is_a? RubyCqrs::Data::Encodable
118
+ data
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,10 @@
1
+ module RubyCqrs
2
+ class NotADomainEventError < Error; end
3
+
4
+ module Domain
5
+ module Event
6
+ include RubyCqrs::Data::Encodable
7
+ attr_reader :aggregate_id, :version
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ module RubyCqrs
2
+ class NotADomainSnapshotError < Error; end
3
+
4
+ module Domain
5
+ module Snapshot
6
+ include RubyCqrs::Data::Encodable
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,48 @@
1
+ module RubyCqrs
2
+ module Domain
3
+ module Snapshotable
4
+ def initialize
5
+ if self.class.const_defined? :SNAPSHOT_THRESHOLD
6
+ @snapshot_threshold = self.class.const_get(:SNAPSHOT_THRESHOLD)
7
+ else
8
+ @snapshot_threshold = 30
9
+ end
10
+ @snapshot_threshold = 30 if @snapshot_threshold <= 0
11
+ @countdown = @snapshot_threshold
12
+ @reset_snapshot_countdown_flag = false
13
+ super
14
+ end
15
+
16
+ private
17
+ def should_take_a_snapshot?
18
+ @countdown <= 0
19
+ end
20
+
21
+ def snapshot_countdown
22
+ @countdown -= 1
23
+ end
24
+
25
+ def reset_countdown loaded_event_count
26
+ @countdown = @snapshot_threshold - loaded_event_count
27
+ @reset_snapshot_countdown_flag = false
28
+ end
29
+
30
+ def should_reset_snapshot_countdown?
31
+ @reset_snapshot_countdown_flag
32
+ end
33
+
34
+ def set_snapshot_taken
35
+ @reset_snapshot_countdown_flag = true
36
+ end
37
+
38
+ # the including domain object should implement these two methods
39
+ def take_a_snapshot
40
+ raise NotImplementedError
41
+ end
42
+
43
+ def apply_snapshot snapshot_object
44
+ raise NotImplementedError
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,3 @@
1
+ module RubyCqrs
2
+ class Error < RuntimeError; end
3
+ end
@@ -0,0 +1,13 @@
1
+ require 'uuidtools'
2
+
3
+ module RubyCqrs
4
+ class Guid
5
+ def self.create
6
+ UUIDTools::UUID.timestamp_create.to_s
7
+ end
8
+
9
+ def self.validate?(guid)
10
+ UUIDTools::UUID.parse(guid).valid?
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module RubyCqrs # :nodoc:
2
+ VERSION = '0.2.0'
3
+ end
data/lib/ruby_cqrs.rb ADDED
@@ -0,0 +1,12 @@
1
+ require('ruby_cqrs/error')
2
+ require('ruby_cqrs/guid')
3
+
4
+ require('ruby_cqrs/data/event_store')
5
+ require('ruby_cqrs/data/serialization')
6
+ require('ruby_cqrs/data/in_memory_event_store')
7
+
8
+ require('ruby_cqrs/domain/event')
9
+ require('ruby_cqrs/domain/snapshot')
10
+ require('ruby_cqrs/domain/aggregate')
11
+ require('ruby_cqrs/domain/snapshotable')
12
+ require('ruby_cqrs/domain/aggregate_repository')
@@ -0,0 +1,177 @@
1
+ require_relative('../spec_helper.rb')
2
+
3
+ # define your domain object
4
+ class Customer
5
+ # mark as a domain object
6
+ include RubyCqrs::Domain::Aggregate
7
+ # makr as snapshotable
8
+ # the default setting is when more 30 events are raised
9
+ # after the last snapshot is taken, a new snapshot is generated
10
+ # change the default by defining SNAPSHOT_THRESHOLD = 20(or other number)
11
+ include RubyCqrs::Domain::Snapshotable
12
+
13
+ attr_reader :name, :credit
14
+
15
+ # unfortunately, you should not try to define your own initialize method
16
+ # at the time being, it could potentially cause error when the aggregate
17
+ # repository try to load your domain object back.
18
+
19
+ # define your domain object's behaviors
20
+ def create_profile name, credit
21
+ # again, this is like your normal initialize method,
22
+ # it should get called only once...
23
+ raise RuntimeError.new('the profile has already been created') unless @name.nil?
24
+ raise AgumentError if name.nil?
25
+ raise AgumentError if credit < 100
26
+ # when business rules are met, raise corresponding event
27
+ raise_event CustomerCreated.new(:name => name, :credit => credit)
28
+ end
29
+
30
+ def order_product price
31
+ raise AgumentErrorr if price <= 0
32
+ raise AgumentError.new("#{@name}'s credit #{@credit} is not enough to pay for a product costs #{price}")\
33
+ if price > @credit
34
+ # when business rules are met, raise corresponding event
35
+ raise_event ProductOrdered.new(:cost => price)
36
+ end
37
+
38
+ private
39
+ # when an event is raised or replayed,
40
+ # these methods will get called automatically,
41
+ # manage the domain object's internal state here
42
+ def on_customer_created customer_created
43
+ @name = customer_created.name
44
+ @credit = customer_created.credit
45
+ end
46
+
47
+ def on_product_ordered product_ordered
48
+ @credit -= product_ordered.cost
49
+ end
50
+
51
+ # when a domain object is marked as snapshotable,
52
+ # you must implement these two methods to record the object's vital state
53
+ # and apply the snapshot in order to restore your object's data respectively
54
+ def take_a_snapshot
55
+ CustomerSnapshot.new(:name => @name, :credit => @credit)
56
+ end
57
+
58
+ def apply_snapshot snapshot_object
59
+ @name = snapshot_object.name
60
+ @credit = snapshot_object.credit
61
+ end
62
+ end
63
+
64
+ # define a snapshot to keep all vital state of your domain object
65
+ # the repository will use the latest snapshot it can find
66
+ # and events happened after the snapshot has been taken
67
+ # to recreate your domain object
68
+ class CustomerSnapshot
69
+ include Beefcake::Message
70
+ include RubyCqrs::Domain::Snapshot
71
+
72
+ required :name, :string, 1
73
+ required :credit, :int32, 2
74
+ end
75
+
76
+ # defined the events your domain object will raise
77
+ class CustomerCreated
78
+ include RubyCqrs::Domain::Event
79
+ include Beefcake::Message
80
+
81
+ required :name, :string, 1
82
+ required :credit, :int32, 2
83
+ end
84
+
85
+ class ProductOrdered
86
+ include RubyCqrs::Domain::Event
87
+ include Beefcake::Message
88
+
89
+ required :cost, :int32, 1
90
+ end
91
+
92
+ # here goes the spec of how you can use the domain object
93
+ describe 'Your awesome customer domain objects powered by ruby_cqrs' do
94
+ # a context related to the command, which is specific to your problem domain
95
+ # normally, when a command arrives, one or more domain objects are created or loaded
96
+ # in order to fulfill the command's request
97
+ let(:command_context) {}
98
+ # you should implement your own event_store in order to persist your aggregate state
99
+ # you should read about the InMemoryEventStore implementation
100
+ # and make sure your response with correct data format
101
+ let(:event_store) { RubyCqrs::Data::InMemoryEventStore.new }
102
+ # every time when a command arrives,
103
+ # an aggregate repository gets created with related command context and event_store implementation
104
+ let(:repository) { RubyCqrs::Domain::AggregateRepository.new event_store, command_context }
105
+
106
+ let(:lucy) do
107
+ instance = Customer.new
108
+ instance.create_profile('Lucy', 1000)
109
+ instance
110
+ end
111
+
112
+ it 'creates a new customer instance' do
113
+ expect(lucy.name).to eq('Lucy')
114
+ expect(lucy.credit).to eq(1000)
115
+
116
+ expect(lucy.version).to eq(1)
117
+ end
118
+
119
+ it 'operates a new customer instance' do
120
+ lucy.order_product 100
121
+
122
+ expect(lucy.name).to eq('Lucy')
123
+ expect(lucy.credit).to eq(900)
124
+
125
+ expect(lucy.version).to eq(2)
126
+ end
127
+
128
+ it 'saves a new customer instance' do
129
+ lucy.order_product 100
130
+ repository.save lucy
131
+
132
+ expect(lucy.name).to eq('Lucy')
133
+ expect(lucy.credit).to eq(900)
134
+ expect(lucy.version).to eq(2)
135
+ end
136
+
137
+ it 'finds an old customer instance by id' do
138
+ lucy.order_product 100
139
+ repository.save lucy
140
+ lucy_reload = repository.find_by lucy.aggregate_id
141
+
142
+ expect(lucy_reload.name).to eq('Lucy')
143
+ expect(lucy_reload.credit).to eq(900)
144
+ expect(lucy_reload.version).to eq(2)
145
+ end
146
+
147
+ it 'operates on an reloaded customer instance' do
148
+ lucy.order_product 100
149
+ repository.save lucy
150
+ lucy_reload = repository.find_by lucy.aggregate_id
151
+
152
+ lucy_reload.order_product 200
153
+
154
+ expect(lucy_reload.name).to eq('Lucy')
155
+ expect(lucy_reload.credit).to eq(700)
156
+ expect(lucy_reload.version).to eq(3)
157
+ end
158
+
159
+ it 'generates a customer snapshot when enough events get fired' do
160
+ (1..30).each { lucy.order_product(10) }
161
+ repository.save lucy
162
+
163
+ # well, you just have to trust a snapshot has been generated :)
164
+ end
165
+
166
+ it 'finds an old customer instance back from snapshot' do
167
+ (1..30).each { lucy.order_product(10) }
168
+ repository.save lucy
169
+ lucy_reload = repository.find_by lucy.aggregate_id
170
+
171
+ # again, it's been loaded from a snapshot ;)
172
+
173
+ expect(lucy_reload.name).to eq('Lucy')
174
+ expect(lucy_reload.credit).to eq(700)
175
+ expect(lucy_reload.version).to eq(31)
176
+ end
177
+ end