sandthorn 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,30 @@
1
+ require "dirty_hashy"
2
+ require "sandthorn/aggregate_root_base"
3
+
4
+ module Sandthorn
5
+ module AggregateRoot
6
+ module DirtyHashy
7
+ include Sandthorn::AggregateRoot::Base
8
+
9
+ def self.included(base)
10
+ base.extend(Sandthorn::AggregateRoot::Base::ClassMethods)
11
+ end
12
+
13
+ def aggregate_initialize
14
+ @hashy = ::DirtyHashy.new
15
+ end
16
+
17
+ def get_delta
18
+ extract_relevant_aggregate_instance_variables.each do |var|
19
+ @hashy[var.to_s.delete("@")] = self.instance_variable_get("#{var}")
20
+ end
21
+ aggregate_attribute_deltas = []
22
+ @hashy.changes.each do |attribute|
23
+ aggregate_attribute_deltas << { :attribute_name => attribute[0], :old_value => attribute[1][0], :new_value => attribute[1][1]}
24
+ end
25
+ aggregate_attribute_deltas
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,39 @@
1
+ module Sandthorn
2
+ module AggregateRootSnapshot
3
+ attr_reader :aggregate_snapshot
4
+
5
+ def aggregate_snapshot!
6
+
7
+ if @aggregate_events.count > 0
8
+ raise "Can't take snapshot on object with unsaved events"
9
+ end
10
+
11
+ @aggregate_snapshot = {
12
+ :event_name => "aggregate_set_from_snapshot",
13
+ :event_args => [self],
14
+ :aggregate_version => @aggregate_current_event_version
15
+ }
16
+ end
17
+
18
+ def save_snapshot
19
+ raise "No snapshot has been created!" unless @aggregate_snapshot
20
+ @aggregate_snapshot[:event_data] = Sandthorn.serialize @aggregate_snapshot[:event_args]
21
+ @aggregate_snapshot[:event_args] = nil
22
+ Sandthorn.save_snapshot @aggregate_snapshot, @aggregate_id, self.class.name
23
+ @aggregate_snapshot = nil
24
+ end
25
+ private
26
+ def aggregate_create_event_when_extended
27
+ self.aggregate_snapshot!
28
+ vars = extract_relevant_aggregate_instance_variables
29
+ vars.each do |var_name|
30
+ value = instance_variable_get var_name
31
+ dump = Marshal.dump(value)
32
+ store_aggregate_instance_variable var_name, dump
33
+ end
34
+ #@aggregate_snapshot[:event_data] = Sandthorn.serialize @aggregate_snapshot[:event_args]
35
+ store_aggregate_event "instance_extended_as_aggregate", @aggregate_snapshot[:event_args]
36
+ @aggregate_snapshot = nil
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,8 @@
1
+ module Sandthorn
2
+ module Errors
3
+ class Error < StandardError; end
4
+ class AggregateNotFound < Error; end
5
+ class ConcurrencyError < Error; end
6
+ class ConfigurationError < Error; end
7
+ end
8
+ end
@@ -0,0 +1,63 @@
1
+ module Sandthorn
2
+ module EventInspector
3
+ def has_unsaved_event? event_name, options = {}
4
+ unsaved = events_with_trace_info
5
+ if self.aggregate_events.empty?
6
+ unsaved = []
7
+ else
8
+ unsaved.reject! { |e| e[:aggregate_version] < self.aggregate_events.first[:aggregate_version] }
9
+ end
10
+ matching_events = unsaved.select { |e| e[:event_name] == event_name }
11
+ event_exists = matching_events.length > 0
12
+ trace = has_trace? matching_events, options.fetch(:trace, {})
13
+
14
+ return event_exists && trace
15
+ end
16
+ def has_saved_event? event_name, options = {}
17
+ saved = events_with_trace_info
18
+ saved.reject! { |e| e[:aggregate_version] >= self.aggregate_events.first[:aggregate_version] } unless self.aggregate_events.empty?
19
+ matching_events = saved.select { |e| e[:event_name] == event_name }
20
+ event_exists = matching_events.length > 0
21
+ trace = has_trace? matching_events, options.fetch(:trace, {})
22
+
23
+ return event_exists && trace
24
+ end
25
+ def has_event? event_name, options = {}
26
+ matching_events = events_with_trace_info.select { |e| e[:event_name] == event_name }
27
+ event_exists = matching_events.length > 0
28
+ trace = has_trace? matching_events, options.fetch(:trace, {})
29
+ return event_exists && trace
30
+ end
31
+ def events_with_trace_info
32
+ saved = Sandthorn.get_aggregate_events self.aggregate_id, self.class
33
+ unsaved = self.aggregate_events
34
+ all = saved.concat(unsaved).sort { |a, b| a[:aggregate_version] <=> b[:aggregate_version] }
35
+ extracted = all.collect do |e|
36
+ if e[:event_args].nil? && !e[:event_data].nil?
37
+ data = Sandthorn.deserialize e[:event_data]
38
+ else
39
+ data = e[:event_args]
40
+ end
41
+ trace = data[:trace] unless data.nil? || !data.is_a?(Hash)
42
+ {aggregate_version: e[:aggregate_version], event_name: e[:event_name].to_sym, trace: trace }
43
+ end
44
+ return extracted
45
+ end
46
+ private
47
+ def get_unsaved_events event_name
48
+ self.aggregate_events.select { |e| e[:event_name] == event_name.to_s }
49
+ end
50
+ def get_saved_events event_name
51
+ saved_events = Sandthorn.get_aggregate_events self.aggregate_id, self.class
52
+ saved_events.select { |e| e[:event_name] == event_name.to_s }
53
+ end
54
+
55
+ def has_trace? events_to_check, trace_info
56
+ return true if trace_info.empty?
57
+ events_to_check.each do |event|
58
+ return false if event[:trace] != trace_info
59
+ end
60
+ true
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,3 @@
1
+ module Sandthorn
2
+ VERSION = "0.0.1"
3
+ end
data/sandthorn.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sandthorn/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sandthorn"
8
+ spec.version = Sandthorn::VERSION
9
+ spec.authors = ["Lars Krantz", "Morgan Hallgren"]
10
+ spec.email = ["lars.krantz@alaz.se", "morgan.hallgren@gmail.com"]
11
+ spec.description = %q{Event sourcing gem}
12
+ spec.summary = %q{Event sourcing gem}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "gem-release"
25
+ spec.add_development_dependency "pry"
26
+ spec.add_development_dependency "pry-doc"
27
+ spec.add_development_dependency "awesome_print"
28
+ spec.add_development_dependency "autotest-standalone"
29
+ spec.add_development_dependency "sqlite3"
30
+ spec.add_development_dependency "sandthorn_driver_sequel"
31
+
32
+ spec.add_runtime_dependency "dirty_hashy"
33
+ spec.add_runtime_dependency "uuidtools"
34
+
35
+ end
@@ -0,0 +1,92 @@
1
+ require 'spec_helper'
2
+ require 'uuidtools'
3
+ require 'sandthorn/aggregate_root_dirty_hashy'
4
+
5
+
6
+
7
+ class PersonTest
8
+ include Sandthorn::AggregateRoot::DirtyHashy
9
+ attr_reader :name
10
+ attr_reader :age
11
+ attr_reader :relationship_status
12
+ attr_reader :my_array
13
+ attr_reader :my_hash
14
+
15
+ def initialize name, age, relationship_status
16
+ @name = name
17
+ @age = age
18
+ @relationship_status = relationship_status
19
+ @my_array = []
20
+ @my_hash = {}
21
+ end
22
+
23
+ def change_name new_name
24
+ @name = new_name
25
+ record_event new_name
26
+ end
27
+
28
+ def change_relationship new_relationship
29
+ @relationship_status = new_relationship
30
+ record_event new_relationship
31
+ end
32
+
33
+ def add_to_array element
34
+ @my_array << element
35
+ record_event element
36
+ end
37
+
38
+ def add_to_hash name,value
39
+ @my_hash[name] = value
40
+ record_event name,value
41
+ end
42
+ end
43
+
44
+ describe 'Property Delta Event Sourcing' do
45
+ let(:person) { PersonTest.new "Lasse",40,:married}
46
+
47
+ it 'should be able to set name' do
48
+ person.change_name "Klabbarparen"
49
+ person.name.should eql("Klabbarparen")
50
+ #puts person.aggregate_events
51
+ end
52
+
53
+ it 'should be able to build from events' do
54
+ person.change_name "Klabbarparen"
55
+ builded = PersonTest.aggregate_build person.aggregate_events
56
+ builded.name.should eql(person.name)
57
+ builded.aggregate_id.should eql(person.aggregate_id)
58
+ end
59
+
60
+ it 'should not have any events when built up' do
61
+ person.change_name "Mattias"
62
+ builded = PersonTest.aggregate_build person.aggregate_events
63
+ builded.aggregate_events.should be_empty
64
+ end
65
+
66
+ it 'should detect change on array' do
67
+ person.add_to_array "Foo"
68
+ person.add_to_array "bar"
69
+
70
+ builded = PersonTest.aggregate_build person.aggregate_events
71
+ builded.my_array.should include "Foo"
72
+ builded.my_array.should include "bar"
73
+ end
74
+
75
+ it 'should detect change on hash' do
76
+ person.add_to_hash :foo, "bar"
77
+ person.add_to_hash :bar, "foo"
78
+
79
+ builded = PersonTest.aggregate_build person.aggregate_events
80
+ builded.my_hash[:foo].should eql("bar")
81
+ builded.my_hash[:bar].should eql("foo")
82
+
83
+ person.add_to_hash :foo, "BAR"
84
+
85
+ #events = person.aggregate_events
86
+ #events << builded.aggregate_events
87
+ #puts events
88
+
89
+ builded2 = PersonTest.aggregate_build person.aggregate_events
90
+ builded2.my_hash[:foo].should eql("BAR")
91
+ end
92
+ end
@@ -0,0 +1,118 @@
1
+ require 'spec_helper'
2
+ require 'sandthorn/aggregate_root_dirty_hashy'
3
+
4
+ module Sandthorn
5
+ module AggregateRoot
6
+ class DirtyClass
7
+ include Sandthorn::AggregateRoot::DirtyHashy
8
+ attr_reader :name, :age
9
+ attr :sex
10
+ attr_writer :writer
11
+
12
+ def initialize args = {}
13
+ @name = args.fetch(:name, nil)
14
+ @sex = args.fetch(:sex, nil)
15
+ @writer = args.fetch(:writer, nil)
16
+ end
17
+
18
+ def change_name value
19
+ unless name == value
20
+ @name = value
21
+ commit
22
+ end
23
+ end
24
+
25
+ def change_sex value
26
+ unless sex == value
27
+ @sex = value
28
+ end
29
+ end
30
+
31
+ def change_writer value
32
+ unless writer == value
33
+ @writer = value
34
+ end
35
+ end
36
+
37
+
38
+ end
39
+
40
+
41
+ describe "when making a change on a aggregate" do
42
+ let(:dirty_obejct) {
43
+ o = DirtyClass.new
44
+ o
45
+ }
46
+
47
+ context "new with args" do
48
+
49
+ let(:subject) { DirtyClass.new(name: "Mogge", sex: "hen", writer: true) }
50
+ it "should set the values" do
51
+ expect(subject.name).to eql "Mogge"
52
+ expect(subject.sex).to eql "hen"
53
+ expect{subject.writer}.to raise_error
54
+ end
55
+ end
56
+
57
+ context "when changing name (attr_reader)" do
58
+
59
+ it "should get new_name" do
60
+ dirty_obejct.change_name "new_name"
61
+ dirty_obejct.name.should eql "new_name"
62
+ end
63
+
64
+ it "should generate one event on new" do
65
+ expect(dirty_obejct.aggregate_events.length).to eql 1
66
+ end
67
+
68
+ it "should generate 2 events new and change_name" do
69
+ dirty_obejct.change_name "new_name"
70
+ expect(dirty_obejct.aggregate_events.length).to eql 2
71
+ end
72
+ end
73
+
74
+ context "when changing sex (attr)" do
75
+ it "should get new_sex" do
76
+ dirty_obejct.change_sex "new_sex"
77
+ dirty_obejct.sex.should eql "new_sex"
78
+ end
79
+ end
80
+
81
+ context "when changing writer (attr_writer)" do
82
+ it "should raise error" do
83
+ expect{dirty_obejct.change_writer "new_writer"}.to raise_error
84
+ end
85
+ end
86
+
87
+ context "save" do
88
+ it "should not have events on aggregete after save" do
89
+ expect(dirty_obejct.save.aggregate_events.length).to eql 0
90
+ end
91
+
92
+ it "should have aggregate_originating_version == 0 pre save" do
93
+ expect(dirty_obejct.aggregate_originating_version).to eql 0
94
+ end
95
+
96
+ it "should have aggregate_originating_version == 1 post save" do
97
+ expect(dirty_obejct.save.aggregate_originating_version).to eql 1
98
+ end
99
+ end
100
+
101
+ context "find" do
102
+ before(:each) { dirty_obejct.save }
103
+ it "should find by id" do
104
+ expect(DirtyClass.find(dirty_obejct.id).id).to eql dirty_obejct.id
105
+ end
106
+
107
+ it "should hold changed name" do
108
+ dirty_obejct.change_name("morgan").save
109
+ expect(DirtyClass.find(dirty_obejct.id).name).to eql "morgan"
110
+ end
111
+
112
+ it "should raise error if trying to find id that not exist" do
113
+ expect{DirtyClass.find("666")}.to raise_error
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,260 @@
1
+ require 'spec_helper'
2
+ require 'uuidtools'
3
+ require 'sandthorn/aggregate_root_dirty_hashy'
4
+ require 'sandthorn/aggregate_root_snapshot'
5
+ require 'date'
6
+
7
+
8
+ module BankAccountInterestCommands
9
+ def calculate_interest! until_date = DateTime.now
10
+ # skipping all safety-checks..
11
+ # and this is of course horribly wrong financially speaking.. whatever
12
+ pay_out_unpaid_interest!
13
+ interest_calculation_time = until_date - @last_interest_calculation
14
+ days_with_interest = interest_calculation_time.to_i
15
+ unpaid_interest = @balance * @current_interest_info[:interest_rate] * days_with_interest / 365.2425
16
+ added_unpaid_interest_event unpaid_interest,until_date
17
+ end
18
+
19
+ def pay_out_unpaid_interest!
20
+ paid_out_unpaid_interest_balance_event @unpaid_interest_balance
21
+ end
22
+
23
+ def change_interest! new_interest_rate, interest_valid_from
24
+ calculate_interest!
25
+ changed_interest_rate_event new_interest_rate,interest_valid_from
26
+ end
27
+ end
28
+ module BankAccountWithdrawalCommands
29
+ def withdraw_from_atm! amount, atm_id
30
+ withdrew_amount_from_atm_event amount, atm_id
31
+ end
32
+
33
+ def withdraw_from_cashier! amount, cashier_id
34
+ withdrew_amount_from_cashier_event amount, cashier_id
35
+ charged_cashier_withdrawal_fee_event 50
36
+ end
37
+ end
38
+
39
+ module BankAccountVisaCardPurchasesCommands
40
+ def charge_card! amount, merchant_id
41
+ visa = VisaCardTransactionGateway.new
42
+ transaction_id = visa.charge_card "3030-3333-4252-2535", merchant_id, amount
43
+ paid_with_visa_card_event amount, transaction_id
44
+ end
45
+
46
+ end
47
+
48
+ class VisaCardTransactionGateway
49
+ def initialize
50
+ @visa_connector = "foo_bar"
51
+ end
52
+ def charge_card visa_card_number, merchant_id, amount
53
+ transaction_id = UUIDTools::UUID.random_create.to_s
54
+ end
55
+ end
56
+
57
+ module BankAccountDepositCommmands
58
+ def deposit_at_bank_office! amount, cashier_id
59
+ deposited_to_cashier_event amount, cashier_id
60
+ end
61
+
62
+ def transfer_money_from_another_account! amount, from_account_number
63
+ incoming_transfer_event amount,from_account_number
64
+ end
65
+ end
66
+
67
+ class BankAccount
68
+ include Sandthorn::AggregateRoot::DirtyHashy
69
+
70
+ attr_reader :balance
71
+ attr_reader :account_number
72
+ attr_reader :current_interest_info
73
+ attr_reader :account_creation_date
74
+ attr_reader :unpaid_interest_balance
75
+ attr_reader :last_interest_calculation
76
+
77
+ def initialize *args
78
+ account_number = args[0]
79
+ interest_rate = args[1]
80
+ creation_date = args[2]
81
+
82
+ @current_interest_info = {}
83
+ @current_interest_info[:interest_rate] = interest_rate
84
+ @current_interest_info[:interest_valid_from] = creation_date
85
+ @balance = 0
86
+ @unpaid_interest_balance = 0
87
+ @account_creation_date = creation_date
88
+ @last_interest_calculation = creation_date
89
+ end
90
+
91
+ def changed_interest_rate_event new_interest_rate, interest_valid_from
92
+ @current_interest_info[:interest_rate] = new_interest_rate
93
+ @current_interest_info[:interest_valid_from] = interest_valid_from
94
+ record_event new_interest_rate,interest_valid_from
95
+ end
96
+
97
+ def added_unpaid_interest_event interest_amount, calculated_until
98
+ @unpaid_interest_balance += interest_amount
99
+ @last_interest_calculation = calculated_until
100
+ record_event interest_amount, calculated_until
101
+ end
102
+
103
+ def paid_out_unpaid_interest_balance_event interest_amount
104
+ @unpaid_interest_balance -= interest_amount
105
+ @balance += interest_amount
106
+ record_event interest_amount
107
+ end
108
+
109
+ def withdrew_amount_from_atm_event amount, atm_id
110
+ @balance -= amount
111
+ record_event amount,atm_id
112
+ end
113
+
114
+ def withdrew_amount_from_cashier_event amount, cashier_id
115
+ @balance -= amount
116
+ record_event amount, cashier_id
117
+ end
118
+
119
+ def paid_with_visa_card_event amount, visa_card_transaction_id
120
+ @balance -= amount
121
+ record_event amount,visa_card_transaction_id
122
+ end
123
+
124
+ def charged_cashier_withdrawal_fee_event amount
125
+ @balance -= amount
126
+ record_event amount
127
+ end
128
+
129
+ def deposited_to_cashier_event amount, cashier_id
130
+ @balance = self.balance + amount
131
+ record_event amount,cashier_id
132
+ end
133
+
134
+ def incoming_transfer_event amount, from_account_number
135
+ current_balance = self.balance
136
+ @balance = amount + current_balance
137
+ record_event amount, from_account_number
138
+ end
139
+
140
+ end
141
+
142
+ def a_test_account
143
+ a = BankAccount.new "91503010111",0.031415, Date.new(2011,10,12)
144
+ a.extend BankAccountDepositCommmands
145
+ a.transfer_money_from_another_account! 90000, "FOOBAR"
146
+ a.deposit_at_bank_office! 10000, "Lars Idorn"
147
+
148
+ a.extend BankAccountVisaCardPurchasesCommands
149
+ a.charge_card! 1000, "Starbucks Coffee"
150
+
151
+ a.extend BankAccountInterestCommands
152
+ a.calculate_interest!
153
+ return a
154
+ end
155
+
156
+ #Tests part
157
+ describe "when doing aggregate_find on an aggregate with a snapshot" do
158
+ let(:aggregate) do
159
+ a = a_test_account
160
+ a.save
161
+ a.extend Sandthorn::AggregateRootSnapshot
162
+ a.aggregate_snapshot!
163
+ a.save_snapshot
164
+ a.charge_card! 9000, "Apple"
165
+ a.save
166
+ a
167
+ end
168
+ it "should be loaded with correct version" do
169
+ org = aggregate
170
+ loaded = BankAccount.find org.aggregate_id
171
+ expect(loaded.balance).to eql org.balance
172
+ end
173
+ end
174
+
175
+ describe 'when generating state on an aggregate root' do
176
+
177
+ before(:each) do
178
+ @original_account = a_test_account
179
+ events = @original_account.aggregate_events
180
+ @account = BankAccount.aggregate_build events
181
+ @account.extend Sandthorn::AggregateRootSnapshot
182
+ @account.aggregate_snapshot!
183
+ end
184
+
185
+ it 'account should have properties set' do
186
+ @account.balance.should eql 99000
187
+ @account.unpaid_interest_balance.should be > 1000
188
+ end
189
+
190
+ it 'should store snapshot data in aggregate_snapshot' do
191
+ @account.aggregate_snapshot.should be_a(Hash)
192
+ end
193
+
194
+ it 'should store aggregate_version in aggregate_snapshot' do
195
+ @account.aggregate_snapshot[:aggregate_version].should eql(@original_account.aggregate_current_event_version)
196
+ end
197
+
198
+ it 'should be able to load up from snapshot' do
199
+
200
+ events = [@account.aggregate_snapshot]
201
+ loaded = BankAccount.aggregate_build events
202
+
203
+ loaded.balance.should eql(@original_account.balance)
204
+ loaded.account_number.should eql(@original_account.account_number)
205
+ loaded.current_interest_info.should eql(@original_account.current_interest_info)
206
+ loaded.account_creation_date.should eql(@original_account.account_creation_date)
207
+ loaded.unpaid_interest_balance.should eql(@original_account.unpaid_interest_balance)
208
+ loaded.last_interest_calculation.should eql(@original_account.last_interest_calculation)
209
+ loaded.aggregate_id.should eql(@original_account.aggregate_id)
210
+ loaded.aggregate_originating_version.should eql(@account.aggregate_originating_version)
211
+
212
+ end
213
+
214
+ end
215
+
216
+ describe 'when saving to repository' do
217
+ let(:account) {a_test_account.extend Sandthorn::AggregateRootSnapshot}
218
+ it 'should raise an error if trying to save before creating a snapshot' do
219
+ lambda {account.save_snapshot}.should raise_error (RuntimeError)
220
+ end
221
+ it 'should not raise an error if snapshot was created' do
222
+ account.save
223
+ account.aggregate_snapshot!
224
+ lambda {account.save_snapshot}.should_not raise_error
225
+ end
226
+ it 'should set aggregate_snapshot to nil' do
227
+ account.save
228
+ account.aggregate_snapshot!
229
+ account.save_snapshot
230
+ account.aggregate_snapshot.should eql(nil)
231
+ end
232
+
233
+ it 'should raise error if trying to create snapshot before events are saved on object' do
234
+ lambda {account.aggregate_snapshot!}.should raise_error
235
+ end
236
+
237
+ it 'should not raise an error if trying to create snapshot on object when events are saved' do
238
+ account.save
239
+ lambda {account.aggregate_snapshot!}.should_not raise_error
240
+ end
241
+
242
+ it 'should get snapshot on account find when a snapshot is saved' do
243
+
244
+ account.save
245
+ account.aggregate_snapshot!
246
+ account.save_snapshot
247
+
248
+ loaded = BankAccount.find account.aggregate_id
249
+
250
+ loaded.balance.should eql(account.balance)
251
+ loaded.account_number.should eql(account.account_number)
252
+ loaded.current_interest_info.should eql(account.current_interest_info)
253
+ loaded.account_creation_date.should eql(account.account_creation_date)
254
+ loaded.unpaid_interest_balance.should eql(account.unpaid_interest_balance)
255
+ loaded.last_interest_calculation.should eql(account.last_interest_calculation)
256
+ loaded.aggregate_id.should eql(account.aggregate_id)
257
+ loaded.aggregate_originating_version.should eql(account.aggregate_originating_version)
258
+
259
+ end
260
+ end