ruby_cqrs 0.2.0

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 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