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 +7 -0
- data/License.txt +21 -0
- data/README.md +33 -0
- data/lib/ruby_cqrs/data/event_store.rb +13 -0
- data/lib/ruby_cqrs/data/in_memory_event_store.rb +70 -0
- data/lib/ruby_cqrs/data/serialization.rb +30 -0
- data/lib/ruby_cqrs/domain/aggregate.rb +107 -0
- data/lib/ruby_cqrs/domain/aggregate_repository.rb +122 -0
- data/lib/ruby_cqrs/domain/event.rb +10 -0
- data/lib/ruby_cqrs/domain/snapshot.rb +9 -0
- data/lib/ruby_cqrs/domain/snapshotable.rb +48 -0
- data/lib/ruby_cqrs/error.rb +3 -0
- data/lib/ruby_cqrs/guid.rb +13 -0
- data/lib/ruby_cqrs/version.rb +3 -0
- data/lib/ruby_cqrs.rb +12 -0
- data/spec/feature/basic_usage_spec.rb +177 -0
- data/spec/feature/snapshot_spec.rb +216 -0
- data/spec/fixture/typical_domain.rb +182 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/support/matchers.rb +8 -0
- data/spec/unit/aggregate_repository_spec.rb +332 -0
- data/spec/unit/aggregate_spec.rb +123 -0
- data/spec/unit/in_memory_event_store_spec.rb +404 -0
- data/spec/unit/serialization_spec.rb +52 -0
- metadata +120 -0
@@ -0,0 +1,216 @@
|
|
1
|
+
require_relative('../spec_helper.rb')
|
2
|
+
|
3
|
+
describe 'Snapshotable module' do
|
4
|
+
let(:command_context) {}
|
5
|
+
let(:repository) { RubyCqrs::Domain::AggregateRepository.new\
|
6
|
+
RubyCqrs::Data::InMemoryEventStore.new, command_context }
|
7
|
+
let(:aggregate_not_snapshot) { SomeDomain::AggregateRootNoSnapshot.new }
|
8
|
+
let(:aggregate_wrong_snapshot) { SomeDomain::AggregateRootWronglyImplementSnapshot.new }
|
9
|
+
let(:aggregate) { SomeDomain::AggregateRoot.new }
|
10
|
+
# an aggregate has a SNAPSHOT_THRESHOLD of 45
|
11
|
+
let(:aggregate_s_45) { SomeDomain::AggregateRoot45Snapshot.new }
|
12
|
+
|
13
|
+
context 'when an aggregate is not snapshotable' do
|
14
|
+
it 'specifies an aggregate is not snapshotable' do
|
15
|
+
expect(aggregate_not_snapshot.is_a? RubyCqrs::Domain::Snapshotable).to be_falsy
|
16
|
+
end
|
17
|
+
|
18
|
+
it "aggregate's #get_changes returns no snapshot field when default amount of events get fired" do
|
19
|
+
(1..30).each { |x| aggregate_not_snapshot.test_fire }
|
20
|
+
changes = aggregate_not_snapshot.send(:get_changes)
|
21
|
+
expect(changes[:snapshot]).to be_nil
|
22
|
+
end
|
23
|
+
|
24
|
+
it "saves and loads the correct aggregate back" do
|
25
|
+
(1..45).each { |x| aggregate_not_snapshot.test_fire }
|
26
|
+
repository.save aggregate_not_snapshot
|
27
|
+
loaded_aggregate = repository.find_by aggregate_not_snapshot.aggregate_id
|
28
|
+
expect(loaded_aggregate.state).to eq(45)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "when an aggregate's snapshot feature is incorrectly implemented" do
|
33
|
+
it "raises NotADomainSnapshotError when enough events get fired" do
|
34
|
+
(1..30).each { |x| aggregate_wrong_snapshot.test_fire }
|
35
|
+
expect{ aggregate_wrong_snapshot.send(:get_changes) }.to\
|
36
|
+
raise_error(RubyCqrs::NotADomainSnapshotError)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'when an aggregate is snapshotable' do
|
41
|
+
it 'specifies an aggregate is snapshotable' do
|
42
|
+
expect(aggregate.is_a? RubyCqrs::Domain::Snapshotable).to be_truthy
|
43
|
+
expect(aggregate_s_45.is_a? RubyCqrs::Domain::Snapshotable).to be_truthy
|
44
|
+
end
|
45
|
+
|
46
|
+
it "aggregate's #get_changes returns an addtional snapshot field when enough events get fired" do
|
47
|
+
(1..30).each { |x| aggregate.test_fire }
|
48
|
+
changes = aggregate.send(:get_changes)
|
49
|
+
expect(changes[:snapshot]).to_not be_nil
|
50
|
+
|
51
|
+
(1..45).each { |x| aggregate_s_45.test_fire }
|
52
|
+
changes = aggregate_s_45.send(:get_changes)
|
53
|
+
expect(changes[:snapshot]).to_not be_nil
|
54
|
+
end
|
55
|
+
|
56
|
+
it "aggregate's #get_changes returns no snapshot field when not enough events get fired" do
|
57
|
+
(1..29).each { |x| aggregate.test_fire }
|
58
|
+
changes = aggregate.send(:get_changes)
|
59
|
+
expect(changes[:snapshot]).to be_nil
|
60
|
+
|
61
|
+
(1..44).each { |x| aggregate_s_45.test_fire }
|
62
|
+
changes = aggregate_s_45.send(:get_changes)
|
63
|
+
expect(changes[:snapshot]).to be_nil
|
64
|
+
end
|
65
|
+
|
66
|
+
it "saves and loads the correct aggregate back with no snapshots are taken" do
|
67
|
+
(1..29).each { |x| aggregate.test_fire }
|
68
|
+
repository.save aggregate
|
69
|
+
loaded_aggregate = repository.find_by aggregate.aggregate_id
|
70
|
+
expect(loaded_aggregate.state).to eq(29)
|
71
|
+
|
72
|
+
(1..44).each { |x| aggregate_s_45.test_fire }
|
73
|
+
repository.save aggregate_s_45
|
74
|
+
loaded_aggregate = repository.find_by aggregate_s_45.aggregate_id
|
75
|
+
expect(loaded_aggregate.state).to eq(44)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "saves and loads the correct aggregate back with 1 snapshot's taken" do
|
79
|
+
(1..45).each { |x| aggregate.test_fire }
|
80
|
+
repository.save aggregate
|
81
|
+
loaded_aggregate = repository.find_by aggregate.aggregate_id
|
82
|
+
expect(loaded_aggregate.state).to eq(45)
|
83
|
+
|
84
|
+
(1..60).each { |x| aggregate_s_45.test_fire }
|
85
|
+
repository.save aggregate_s_45
|
86
|
+
loaded_aggregate = repository.find_by aggregate_s_45.aggregate_id
|
87
|
+
expect(loaded_aggregate.state).to eq(60)
|
88
|
+
end
|
89
|
+
|
90
|
+
it "saves and loads the correct aggregate back incrementally and eventually triggers a snapshot" do
|
91
|
+
(1..15).each { |x| aggregate.test_fire }
|
92
|
+
repository.save aggregate
|
93
|
+
loaded_aggregate = repository.find_by aggregate.aggregate_id
|
94
|
+
expect(loaded_aggregate.state).to eq(15)
|
95
|
+
|
96
|
+
(1..15).each { |x| loaded_aggregate.test_fire }
|
97
|
+
repository.save loaded_aggregate
|
98
|
+
loaded_aggregate = repository.find_by aggregate.aggregate_id
|
99
|
+
expect(loaded_aggregate.state).to eq(30)
|
100
|
+
|
101
|
+
(1..30).each { |x| aggregate_s_45.test_fire }
|
102
|
+
repository.save aggregate_s_45
|
103
|
+
loaded_aggregate = repository.find_by aggregate_s_45.aggregate_id
|
104
|
+
expect(loaded_aggregate.state).to eq(30)
|
105
|
+
|
106
|
+
(1..15).each { |x| loaded_aggregate.test_fire }
|
107
|
+
repository.save loaded_aggregate
|
108
|
+
loaded_aggregate = repository.find_by aggregate_s_45.aggregate_id
|
109
|
+
expect(loaded_aggregate.state).to eq(45)
|
110
|
+
end
|
111
|
+
|
112
|
+
it "saves aggregate incrementally and eventually triggers a snapshot, without reloading aggregate during the process" do
|
113
|
+
(1..15).each { |x| aggregate.test_fire }
|
114
|
+
repository.save aggregate
|
115
|
+
expect(aggregate.state).to eq(15)
|
116
|
+
|
117
|
+
(1..15).each { |x| aggregate.test_fire }
|
118
|
+
repository.save aggregate
|
119
|
+
expect(aggregate.state).to eq(30)
|
120
|
+
|
121
|
+
loaded_aggregate = repository.find_by aggregate.aggregate_id
|
122
|
+
expect(loaded_aggregate.state).to eq(30)
|
123
|
+
|
124
|
+
(1..30).each { |x| aggregate_s_45.test_fire }
|
125
|
+
repository.save aggregate_s_45
|
126
|
+
expect(aggregate_s_45.state).to eq(30)
|
127
|
+
|
128
|
+
(1..15).each { |x| aggregate_s_45.test_fire }
|
129
|
+
repository.save aggregate_s_45
|
130
|
+
expect(aggregate_s_45.state).to eq(45)
|
131
|
+
|
132
|
+
loaded_aggregate = repository.find_by aggregate_s_45.aggregate_id
|
133
|
+
expect(loaded_aggregate.state).to eq(45)
|
134
|
+
end
|
135
|
+
|
136
|
+
it "saves and loads the correct aggregate back with two snapshot taken" do
|
137
|
+
(1..30).each { |x| aggregate.test_fire }
|
138
|
+
repository.save aggregate
|
139
|
+
loaded_aggregate = repository.find_by aggregate.aggregate_id
|
140
|
+
expect(loaded_aggregate.state).to eq(30)
|
141
|
+
|
142
|
+
(1..30).each { |x| loaded_aggregate.test_fire }
|
143
|
+
repository.save loaded_aggregate
|
144
|
+
loaded_aggregate = repository.find_by aggregate.aggregate_id
|
145
|
+
expect(loaded_aggregate.state).to eq(60)
|
146
|
+
|
147
|
+
(1..45).each { |x| aggregate_s_45.test_fire }
|
148
|
+
repository.save aggregate_s_45
|
149
|
+
loaded_aggregate = repository.find_by aggregate_s_45.aggregate_id
|
150
|
+
expect(loaded_aggregate.state).to eq(45)
|
151
|
+
|
152
|
+
(1..45).each { |x| loaded_aggregate.test_fire }
|
153
|
+
repository.save loaded_aggregate
|
154
|
+
loaded_aggregate = repository.find_by aggregate_s_45.aggregate_id
|
155
|
+
expect(loaded_aggregate.state).to eq(90)
|
156
|
+
end
|
157
|
+
|
158
|
+
it "saves and loads the correct aggregate back incrementally and eventually triggers two snapshot" do
|
159
|
+
(1..30).each { |x| aggregate.test_fire }
|
160
|
+
repository.save aggregate
|
161
|
+
loaded_aggregate = repository.find_by aggregate.aggregate_id
|
162
|
+
expect(loaded_aggregate.state).to eq(30)
|
163
|
+
|
164
|
+
(1..15).each { |x| loaded_aggregate.test_fire }
|
165
|
+
repository.save loaded_aggregate
|
166
|
+
loaded_aggregate = repository.find_by aggregate.aggregate_id
|
167
|
+
expect(loaded_aggregate.state).to eq(45)
|
168
|
+
|
169
|
+
(1..15).each { |x| loaded_aggregate.test_fire }
|
170
|
+
repository.save loaded_aggregate
|
171
|
+
loaded_aggregate = repository.find_by aggregate.aggregate_id
|
172
|
+
expect(loaded_aggregate.state).to eq(60)
|
173
|
+
|
174
|
+
(1..45).each { |x| aggregate_s_45.test_fire }
|
175
|
+
repository.save aggregate_s_45
|
176
|
+
loaded_aggregate = repository.find_by aggregate_s_45.aggregate_id
|
177
|
+
expect(loaded_aggregate.state).to eq(45)
|
178
|
+
|
179
|
+
(1..30).each { |x| loaded_aggregate.test_fire }
|
180
|
+
repository.save loaded_aggregate
|
181
|
+
loaded_aggregate = repository.find_by aggregate_s_45.aggregate_id
|
182
|
+
expect(loaded_aggregate.state).to eq(75)
|
183
|
+
|
184
|
+
(1..15).each { |x| loaded_aggregate.test_fire }
|
185
|
+
repository.save loaded_aggregate
|
186
|
+
loaded_aggregate = repository.find_by aggregate_s_45.aggregate_id
|
187
|
+
expect(loaded_aggregate.state).to eq(90)
|
188
|
+
end
|
189
|
+
|
190
|
+
it "saves and loads the correct aggregate back incrementally and eventually triggers two snapshot, without reloading aggregate during the process" do
|
191
|
+
(1..30).each { |x| aggregate.test_fire }
|
192
|
+
repository.save aggregate
|
193
|
+
expect(aggregate.state).to eq(30)
|
194
|
+
|
195
|
+
(1..15).each { |x| aggregate.test_fire }
|
196
|
+
repository.save aggregate
|
197
|
+
expect(aggregate.state).to eq(45)
|
198
|
+
|
199
|
+
(1..15).each { |x| aggregate.test_fire }
|
200
|
+
repository.save aggregate
|
201
|
+
expect(aggregate.state).to eq(60)
|
202
|
+
|
203
|
+
(1..45).each { |x| aggregate_s_45.test_fire }
|
204
|
+
repository.save aggregate_s_45
|
205
|
+
expect(aggregate_s_45.state).to eq(45)
|
206
|
+
|
207
|
+
(1..30).each { |x| aggregate_s_45.test_fire }
|
208
|
+
repository.save aggregate_s_45
|
209
|
+
expect(aggregate_s_45.state).to eq(75)
|
210
|
+
|
211
|
+
(1..15).each { |x| aggregate_s_45.test_fire }
|
212
|
+
repository.save aggregate_s_45
|
213
|
+
expect(aggregate_s_45.state).to eq(90)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
require 'beefcake'
|
2
|
+
|
3
|
+
module SomeDomain
|
4
|
+
class AggregateRoot
|
5
|
+
include RubyCqrs::Domain::Aggregate
|
6
|
+
include RubyCqrs::Domain::Snapshotable
|
7
|
+
|
8
|
+
attr_reader :state
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@state = 0
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def fire_weird_stuff
|
16
|
+
raise_event Object.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_fire
|
20
|
+
raise_event THIRD_EVENT_INSTANCE
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_fire_ag
|
24
|
+
raise_event FORTH_EVENT_INSTANCE
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def on_first_event event; @state += 1; end
|
29
|
+
def on_second_event event; @state += 1; end
|
30
|
+
def on_third_event event; @state += 1; end
|
31
|
+
def on_forth_event event; @state += 1; end
|
32
|
+
|
33
|
+
def take_a_snapshot
|
34
|
+
SomeSnapshot.new(:state => @state)
|
35
|
+
end
|
36
|
+
|
37
|
+
def apply_snapshot snapshot_object
|
38
|
+
@state = snapshot_object.state
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class AggregateRoot45Snapshot
|
43
|
+
include RubyCqrs::Domain::Aggregate
|
44
|
+
include RubyCqrs::Domain::Snapshotable
|
45
|
+
|
46
|
+
SNAPSHOT_THRESHOLD = 45
|
47
|
+
|
48
|
+
attr_reader :state
|
49
|
+
|
50
|
+
def initialize
|
51
|
+
@state = 0
|
52
|
+
super
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_fire
|
56
|
+
raise_event THIRD_EVENT_INSTANCE
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def on_third_event event; @state += 1; end
|
61
|
+
|
62
|
+
def take_a_snapshot
|
63
|
+
SomeSnapshot.new(:state => @state)
|
64
|
+
end
|
65
|
+
|
66
|
+
def apply_snapshot snapshot_object
|
67
|
+
@state = snapshot_object.state
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class AggregateRootWronglyImplementSnapshot
|
72
|
+
include RubyCqrs::Domain::Aggregate
|
73
|
+
include RubyCqrs::Domain::Snapshotable
|
74
|
+
|
75
|
+
attr_reader :state
|
76
|
+
|
77
|
+
def initialize
|
78
|
+
@state = 0
|
79
|
+
super
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_fire
|
83
|
+
raise_event THIRD_EVENT_INSTANCE
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
def on_third_event event; @state += 1; end
|
88
|
+
|
89
|
+
def take_a_snapshot
|
90
|
+
WrongSnapshot.new(:state => @state)
|
91
|
+
end
|
92
|
+
|
93
|
+
def apply_snapshot snapshot_object
|
94
|
+
@state = snapshot_object.state
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class AggregateRootNoSnapshot
|
99
|
+
include RubyCqrs::Domain::Aggregate
|
100
|
+
|
101
|
+
attr_reader :state
|
102
|
+
|
103
|
+
def initialize
|
104
|
+
@state = 0
|
105
|
+
super
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_fire
|
109
|
+
raise_event THIRD_EVENT_INSTANCE
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
def on_third_event event; @state += 1; end
|
114
|
+
end
|
115
|
+
|
116
|
+
class SomeSnapshot
|
117
|
+
include Beefcake::Message
|
118
|
+
include RubyCqrs::Domain::Snapshot
|
119
|
+
|
120
|
+
required :state, :int32, 1
|
121
|
+
end
|
122
|
+
|
123
|
+
class WrongSnapshot
|
124
|
+
include Beefcake::Message
|
125
|
+
|
126
|
+
required :state, :int32, 1
|
127
|
+
end
|
128
|
+
|
129
|
+
AGGREGATE_ID = 'cbb688cc-d49a-11e4-9f39-3c15c2d13d4e'
|
130
|
+
|
131
|
+
class FirstEvent
|
132
|
+
include RubyCqrs::Domain::Event
|
133
|
+
include Beefcake::Message
|
134
|
+
|
135
|
+
def initialize
|
136
|
+
@aggregate_id = AGGREGATE_ID
|
137
|
+
@version = 1
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class SecondEvent
|
142
|
+
include RubyCqrs::Domain::Event
|
143
|
+
include Beefcake::Message
|
144
|
+
|
145
|
+
def initialize
|
146
|
+
@aggregate_id = AGGREGATE_ID
|
147
|
+
@version = 2
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
class ThirdEvent
|
152
|
+
include RubyCqrs::Domain::Event
|
153
|
+
include Beefcake::Message
|
154
|
+
|
155
|
+
required :id, :int32, 1
|
156
|
+
required :name, :string, 2
|
157
|
+
required :phone, :string, 3
|
158
|
+
|
159
|
+
optional :note, :string, 4
|
160
|
+
end
|
161
|
+
|
162
|
+
class ForthEvent
|
163
|
+
include RubyCqrs::Domain::Event
|
164
|
+
include Beefcake::Message
|
165
|
+
|
166
|
+
required :order_id, :int32, 1
|
167
|
+
required :price, :int32, 2
|
168
|
+
required :customer_id,:int32, 3
|
169
|
+
|
170
|
+
optional :note, :string, 4
|
171
|
+
end
|
172
|
+
|
173
|
+
class FifthEvent
|
174
|
+
include RubyCqrs::Domain::Event
|
175
|
+
end
|
176
|
+
|
177
|
+
THIRD_EVENT_INSTANCE = ThirdEvent.new(:id => 1, :name => 'some dude',\
|
178
|
+
:phone => '13322244444', :note => 'good luck')
|
179
|
+
|
180
|
+
FORTH_EVENT_INSTANCE = ForthEvent.new(:order_id => 100, :price => 2000,\
|
181
|
+
:customer_id => 1, :note => 'sold!')
|
182
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "codeclimate-test-reporter"
|
2
|
+
CodeClimate::TestReporter.start
|
3
|
+
|
4
|
+
require 'bundler'
|
5
|
+
Bundler.setup(:default, :test)
|
6
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
7
|
+
|
8
|
+
require('ruby_cqrs')
|
9
|
+
|
10
|
+
require('fixture/typical_domain')
|
11
|
+
require('support/matchers')
|