sequent 0.1.1 → 0.1.2

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/lib/sequent/core/aggregate_repository.rb +94 -0
  3. data/lib/sequent/core/aggregate_root.rb +87 -0
  4. data/lib/sequent/core/base_command_handler.rb +39 -0
  5. data/lib/sequent/core/base_event_handler.rb +51 -0
  6. data/lib/sequent/core/command.rb +79 -0
  7. data/lib/sequent/core/command_record.rb +26 -0
  8. data/lib/sequent/core/command_service.rb +118 -0
  9. data/lib/sequent/core/core.rb +15 -0
  10. data/lib/sequent/core/event.rb +62 -0
  11. data/lib/sequent/core/event_record.rb +34 -0
  12. data/lib/sequent/core/event_store.rb +110 -0
  13. data/lib/sequent/core/helpers/association_validator.rb +39 -0
  14. data/lib/sequent/core/helpers/attribute_support.rb +207 -0
  15. data/lib/sequent/core/helpers/boolean_support.rb +36 -0
  16. data/lib/sequent/core/helpers/copyable.rb +25 -0
  17. data/lib/sequent/core/helpers/equal_support.rb +41 -0
  18. data/lib/sequent/core/helpers/helpers.rb +9 -0
  19. data/lib/sequent/core/helpers/mergable.rb +21 -0
  20. data/lib/sequent/core/helpers/param_support.rb +80 -0
  21. data/lib/sequent/core/helpers/self_applier.rb +45 -0
  22. data/lib/sequent/core/helpers/string_support.rb +22 -0
  23. data/lib/sequent/core/helpers/uuid_helper.rb +17 -0
  24. data/lib/sequent/core/record_sessions/active_record_session.rb +92 -0
  25. data/lib/sequent/core/record_sessions/record_sessions.rb +2 -0
  26. data/lib/sequent/core/record_sessions/replay_events_session.rb +306 -0
  27. data/lib/sequent/core/tenant_event_store.rb +18 -0
  28. data/lib/sequent/core/transactions/active_record_transaction_provider.rb +16 -0
  29. data/lib/sequent/core/transactions/no_transactions.rb +13 -0
  30. data/lib/sequent/core/transactions/transactions.rb +2 -0
  31. data/lib/sequent/core/value_object.rb +48 -0
  32. data/lib/sequent/migrations/migrate_events.rb +53 -0
  33. data/lib/sequent/migrations/migrations.rb +7 -0
  34. data/lib/sequent/sequent.rb +3 -0
  35. data/lib/sequent/test/command_handler_helpers.rb +101 -0
  36. data/lib/sequent/test/test.rb +1 -0
  37. data/lib/version.rb +3 -0
  38. metadata +38 -3
@@ -0,0 +1,92 @@
1
+ require 'active_record'
2
+
3
+ module Sequent
4
+ module Core
5
+ module RecordSessions
6
+ #
7
+ # Session objects are used to update view state
8
+ #
9
+ # The ActiveRecordSession object can be used when you use ActiveRecord as view state store.
10
+ #
11
+ class ActiveRecordSession
12
+
13
+ def update_record(record_class, event, where_clause = {aggregate_id: event.aggregate_id}, options = {}, &block)
14
+ defaults = {update_sequence_number: true}
15
+ args = defaults.merge(options)
16
+ record = record_class.unscoped.where(where_clause).first
17
+ raise("Record of class #{record_class} with where clause #{where_clause} not found while handling event #{event}") unless record
18
+ yield record if block_given?
19
+ record.sequence_number = event.sequence_number if args[:update_sequence_number]
20
+ record.updated_at = event.created_at if record.respond_to?(:updated_at)
21
+ record.save!
22
+ end
23
+
24
+ def create_record(record_class, values)
25
+ record = new_record(record_class, values)
26
+ yield record if block_given?
27
+ record.save!
28
+ record
29
+ end
30
+
31
+ def create_or_update_record(record_class, values, created_at = Time.now)
32
+ record = get_record(record_class, values)
33
+ unless record
34
+ record = new_record(record_class, values.merge(created_at: created_at))
35
+ end
36
+ yield record
37
+ record.save!
38
+ record
39
+ end
40
+
41
+ def get_record!(record_class, where_clause)
42
+ record_class.unscoped.where(where_clause).first!
43
+ end
44
+
45
+ def get_record(record_class, where_clause)
46
+ record_class.unscoped.where(where_clause).first
47
+ end
48
+
49
+ def delete_all_records(record_class, where_clause)
50
+ record_class.unscoped.where(where_clause).delete_all
51
+ end
52
+
53
+ def delete_record(_, record)
54
+ record.destroy
55
+ end
56
+
57
+ def update_all_records(record_class, where_clause, updates)
58
+ record_class.unscoped.where(where_clause).update_all(updates)
59
+ end
60
+
61
+ def do_with_records(record_class, where_clause)
62
+ record_class.unscoped.where(where_clause).each do |record|
63
+ yield record
64
+ record.save!
65
+ end
66
+ end
67
+
68
+ def do_with_record(record_class, where_clause)
69
+ record = record_class.unscoped.where(where_clause).first!
70
+ yield record
71
+ record.save!
72
+ end
73
+
74
+ def find_records(record_class, where_clause)
75
+ record_class.unscoped.where(where_clause)
76
+ end
77
+
78
+ def last_record(record_class, where_clause)
79
+ record_class.unscoped.where(where_clause).last
80
+ end
81
+
82
+ private
83
+
84
+ def new_record(record_class, values)
85
+ record_class.unscoped.new(values)
86
+ end
87
+
88
+ end
89
+
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'active_record_session'
2
+ require_relative 'replay_events_session'
@@ -0,0 +1,306 @@
1
+ require 'set'
2
+ require 'active_record'
3
+
4
+ module Sequent
5
+ module Core
6
+ module RecordSessions
7
+ #
8
+ # Session objects are used to update view state
9
+ #
10
+ # The ReplayEventsSession is optimized for bulk loading records in a Postgres database using CSV import.
11
+ #
12
+ # After lot of experimenting this turned out to be the fastest way to to bulk inserts in the database.
13
+ # You can tweak the amount of records in the CSV via +insert_with_csv_size+ before
14
+ # it flushes to the database to gain (or loose) speed.
15
+ #
16
+ # It is highly recommended to create +indices+ on the in memory +record_store+ to speed up the processing.
17
+ # By default all records are indexed by +aggregate_id+ if they have such a property.
18
+ #
19
+ # Example:
20
+ #
21
+ # class InvoiceEventHandler < Sequent::Core::BaseEventHandler
22
+ # on RecipientMovedEvent do |event|
23
+ # update_all_records InvoiceRecord, recipient_id: event.recipient.aggregate_id do |record|
24
+ # record.recipient_street = record.recipient.street
25
+ # end
26
+ # end
27
+ # end
28
+ #
29
+ # In this case it is wise to create an index on InvoiceRecord on the recipient_id like you would in the database.
30
+ #
31
+ # Example:
32
+ #
33
+ # ReplayEventsSession.new(
34
+ # 50,
35
+ # {InvoiceRecord => [[:recipient_id]]}
36
+ # )
37
+ class ReplayEventsSession
38
+
39
+ attr_reader :record_store
40
+ attr_accessor :insert_with_csv_size
41
+
42
+ def self.struct_cache
43
+ @struct_cache ||= {}
44
+ end
45
+
46
+ module InitStruct
47
+ def set_values(values)
48
+ values.each do |k, v|
49
+ self[k] = v
50
+ end
51
+ self
52
+ end
53
+ end
54
+
55
+ # +insert_with_csv_size+ number of records to insert in a single batch
56
+ #
57
+ # +indices+ Hash of indices to create in memory. Greatly speeds up the replaying.
58
+ # Key corresponds to the name of the 'Record'
59
+ # Values contains list of lists on which columns to index. E.g. [[:first_index_column], [:another_index, :with_to_columns]]
60
+ def initialize(insert_with_csv_size = 50, indices = {})
61
+ @insert_with_csv_size = insert_with_csv_size
62
+ @record_store = Hash.new { |h, k| h[k] = Set.new }
63
+ @record_index = {}
64
+ @indices = indices
65
+ end
66
+
67
+ def update_record(record_class, event, where_clause = {aggregate_id: event.aggregate_id}, options = {}, &block)
68
+ defaults = {update_sequence_number: true}
69
+ args = defaults.merge(options)
70
+ record = get_record!(record_class, where_clause)
71
+ record.updated_at = event.created_at if record.respond_to?(:updated_at)
72
+ yield record if block_given?
73
+ record.sequence_number = event.sequence_number if args[:update_sequence_number]
74
+ end
75
+
76
+ def create_record(record_class, values)
77
+ column_names = record_class.column_names
78
+ values.merge!(updated_at: values[:created_at]) if column_names.include?("updated_at")
79
+ struct_class_name = "#{record_class.to_s}Struct"
80
+ if self.class.struct_cache.has_key?(struct_class_name)
81
+ struct_class = self.class.struct_cache[struct_class_name]
82
+ else
83
+
84
+ # We create a struct on the fly.
85
+ # Since the replay happens in memory we implement the ==, eql? and hash methods
86
+ # to point to the same object. A record is the same if and only if they point to
87
+ # the same object. These methods are necessary since we use Set instead of [].
88
+ class_def=<<-EOD
89
+ #{struct_class_name} = Struct.new(*#{column_names.map(&:to_sym)})
90
+ class #{struct_class_name}
91
+ include InitStruct
92
+ def ==(other)
93
+ return true if self.equal?(other)
94
+ super
95
+ end
96
+ def eql?(other)
97
+ self == other
98
+ end
99
+ def hash
100
+ self.object_id.hash
101
+ end
102
+ end
103
+ EOD
104
+ eval("#{class_def}")
105
+ struct_class = ReplayEventsSession.const_get(struct_class_name)
106
+ self.class.struct_cache[struct_class_name] = struct_class
107
+ end
108
+ record = struct_class.new.set_values(values)
109
+
110
+ yield record if block_given?
111
+ @record_store[record_class] << record
112
+ if record.respond_to? :aggregate_id
113
+ @record_index[[record_class, record.aggregate_id]] = record
114
+ end
115
+
116
+ if indexed?(record_class)
117
+ do_with_cache_keys(record_class, record) do |key|
118
+ @record_index[key] = [] unless @record_index.has_key?(key)
119
+ @record_index[key] << record
120
+ end
121
+ end
122
+ record
123
+ end
124
+
125
+ def create_or_update_record(record_class, values, created_at = Time.now)
126
+ record = get_record(record_class, values)
127
+ unless record
128
+ record = create_record(record_class, values.merge(created_at: created_at))
129
+ end
130
+ yield record if block_given?
131
+ record
132
+ end
133
+
134
+ def get_record!(record_class, where_clause)
135
+ record = get_record(record_class, where_clause)
136
+ raise("record #{record_class} not found for #{where_clause}, store: #{@record_store[record_class]}") unless record
137
+ record
138
+ end
139
+
140
+ def get_record(record_class, where_clause)
141
+ results = find_records(record_class, where_clause)
142
+ results.empty? ? nil : results.first
143
+ end
144
+
145
+ def delete_all_records(record_class, where_clause)
146
+ find_records(record_class, where_clause).each do |record|
147
+ delete_record(record_class, record)
148
+ end
149
+ end
150
+
151
+ def delete_record(record_class, record)
152
+ @record_store[record_class].delete(record)
153
+ if indexed?(record_class)
154
+ do_with_cache_keys(record_class, record) do |key|
155
+ @record_index[key].delete(record) if @record_index.has_key?(key)
156
+ end
157
+ end
158
+ end
159
+
160
+ def update_all_records(record_class, where_clause, updates)
161
+ find_records(record_class, where_clause).each do |record|
162
+ updates.each_pair do |k, v|
163
+ record[k.to_sym] = v
164
+ end
165
+ end
166
+ end
167
+
168
+ def do_with_records(record_class, where_clause)
169
+ records = find_records(record_class, where_clause)
170
+ records.each do |record|
171
+ yield record
172
+ end
173
+ end
174
+
175
+ def do_with_record(record_class, where_clause)
176
+ record = get_record!(record_class, where_clause)
177
+ yield record
178
+ end
179
+
180
+ def find_records(record_class, where_clause)
181
+ if where_clause.has_key? :aggregate_id and where_clause.size == 1
182
+ [@record_index[[record_class, where_clause[:aggregate_id]]]].compact
183
+ elsif use_index?(record_class, where_clause)
184
+ values = get_index(record_class, where_clause).map { |field| where_clause[field] }
185
+ @record_index[[record_class, *values]] || []
186
+ else
187
+ @record_store[record_class].select do |record|
188
+ where_clause.all? do |k, v|
189
+ expected_value = v.kind_of?(Symbol) ? v.to_s : v
190
+ actual_value = record[k.to_sym]
191
+ actual_value = actual_value.to_s if actual_value.kind_of? Symbol
192
+ if expected_value.kind_of?(Array)
193
+ expected_value.include?(actual_value)
194
+ else
195
+ actual_value == expected_value
196
+ end
197
+ end
198
+
199
+ end
200
+ end.dup
201
+ end
202
+
203
+ def last_record(record_class, where_clause)
204
+ results = find_records(record_class, where_clause)
205
+ results.empty? ? nil : results.last
206
+ end
207
+
208
+ def commit
209
+ begin
210
+ @record_store.each do |clazz, records|
211
+ if records.size > @insert_with_csv_size
212
+ csv = CSV.new("")
213
+ column_names = clazz.column_names.reject { |name| name == "id" }
214
+ records.each do |obj|
215
+ begin
216
+ csv << column_names.map do |column_name|
217
+ obj[column_name]
218
+ end
219
+ end
220
+ end
221
+
222
+ buf = ''
223
+ conn = ActiveRecord::Base.connection.raw_connection
224
+ copy_data = StringIO.new csv.string
225
+ conn.transaction do
226
+ conn.exec("COPY #{clazz.table_name} (#{column_names.join(",")}) FROM STDIN WITH csv")
227
+ begin
228
+ while copy_data.read(1024, buf)
229
+ ### Uncomment this to test error-handling for exceptions from the reader side:
230
+ # raise Errno::ECONNRESET, "socket closed while reading"
231
+ until conn.put_copy_data(buf)
232
+ sleep 0.1
233
+ end
234
+ end
235
+ rescue Errno => err
236
+ errmsg = "%s while reading copy data: %s" % [err.class.name, err.message]
237
+ conn.put_copy_end(errmsg)
238
+ ensure
239
+ conn.put_copy_end
240
+ copy_data.close
241
+ while res = conn.get_result
242
+ status = res.res_status(res.result_status)
243
+ if status != "PGRES_COMMAND_OK"
244
+ raise "Postgres copy command failed: #{status}, #{res.error_message}"
245
+ end
246
+ end
247
+ end
248
+ end
249
+
250
+ else
251
+
252
+ clazz.unscoped do
253
+ inserts = []
254
+ column_names = clazz.column_names.reject { |name| name == "id" }
255
+ prepared_values = (1..column_names.size).map { |i| "$#{i}" }.join(",")
256
+ records.each do |r|
257
+ values = column_names.map { |name| r[name.to_sym] }
258
+ inserts << values
259
+ end
260
+ sql = %Q{insert into #{clazz.table_name} (#{column_names.join(",")}) values (#{prepared_values})}
261
+ inserts.each do |insert|
262
+ clazz.connection.raw_connection.async_exec(sql, insert)
263
+ end
264
+ end
265
+ end
266
+ end
267
+
268
+
269
+ ensure
270
+ clear
271
+ end
272
+ end
273
+
274
+ def clear
275
+ @record_store.clear
276
+ @record_index.clear
277
+ end
278
+
279
+ private
280
+ def indexed?(record_class)
281
+ @indices.has_key?(record_class)
282
+ end
283
+
284
+ def do_with_cache_keys(record_class, record)
285
+ @indices[record_class].each do |index|
286
+ cache_key = [record_class]
287
+ index.each do |key|
288
+ cache_key << record[key]
289
+ end
290
+ yield cache_key
291
+ end
292
+ end
293
+
294
+ def use_index?(record_class, where_clause)
295
+ @indices.has_key?(record_class) and @indices[record_class].any? { |indexed_where| where_clause.keys.size == indexed_where.size and (where_clause.keys - indexed_where).empty? }
296
+ end
297
+
298
+ def get_index(record_class, where_clause)
299
+ @indices[record_class].find { |indexed_where| where_clause.keys.size == indexed_where.size and (where_clause.keys - indexed_where).empty? }
300
+ end
301
+
302
+ end
303
+
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,18 @@
1
+ ##
2
+ # Multi-tenant event store that replays events grouped by a specific tenant.
3
+ #
4
+ module Sequent
5
+ module Core
6
+ class TenantEventStore < EventStore
7
+
8
+ def replay_events_for(organization_id)
9
+ replay_events do
10
+ aggregate_ids = @record_class.connection.select_all("select distinct aggregate_id from #{@record_class.table_name} where organization_id = '#{organization_id}'").map { |hash| hash["aggregate_id"] }
11
+ @record_class.connection.select_all("select id, event_type, event_json from #{@record_class.table_name} where aggregate_id in (#{aggregate_ids.map { |id| %Q{'#{id}'} }.join(",")}) order by id")
12
+ end
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ module Sequent
2
+ module Core
3
+ module Transactions
4
+
5
+ class ActiveRecordTransactionProvider
6
+ def transactional
7
+ ActiveRecord::Base.transaction do
8
+ yield
9
+ end
10
+ end
11
+
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ module Sequent
2
+ module Core
3
+ module Transactions
4
+
5
+ class NoTransactions
6
+ def transactional
7
+ yield
8
+ end
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'active_record_transaction_provider'
2
+ require_relative 'no_transactions'
@@ -0,0 +1,48 @@
1
+ require 'active_model'
2
+ require_relative 'helpers/string_support'
3
+ require_relative 'helpers/equal_support'
4
+ require_relative 'helpers/copyable'
5
+ require_relative 'helpers/attribute_support'
6
+ require_relative 'helpers/param_support'
7
+
8
+ module Sequent
9
+
10
+ module Core
11
+ #
12
+ # ValueObject is a container for data that belongs together but requires no identity
13
+ #
14
+ # If something requires identity is up to you to decide. An example in for instance
15
+ # the invoicing domain could be a person's Address.
16
+ #
17
+ # class Address < Sequent::Core::ValueObject
18
+ # attrs street: String, city: String, country: Country
19
+ # end
20
+ #
21
+ # A ValueObject is equal to another ValueObject if and only if all +attrs+ are equal.
22
+ #
23
+ # You can copy a valueobject as follows:
24
+ #
25
+ # new_address = address.copy(street: "New Street")
26
+ #
27
+ # This a deep clone of the address with the street attribute containing "New Street"
28
+ class ValueObject
29
+ include Sequent::Core::Helpers::StringSupport,
30
+ Sequent::Core::Helpers::EqualSupport,
31
+ Sequent::Core::Helpers::Copyable,
32
+ Sequent::Core::Helpers::AttributeSupport,
33
+ Sequent::Core::Helpers::ParamSupport,
34
+ ActiveModel::Serializers::JSON,
35
+ ActiveModel::Validations
36
+
37
+ self.include_root_in_json=false
38
+
39
+ def initialize(args = {})
40
+ @errors = ActiveModel::Errors.new(self)
41
+ update_all_attributes args
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+ end
48
+
@@ -0,0 +1,53 @@
1
+ ##
2
+ # When you need to upgrade the event store based on information of the previous schema version
3
+ # this is the place you need to implement a migration.
4
+ # Examples are: corrupt events (due to insufficient testing for instance...)
5
+ # or adding extra events to the event stream if a new concept is introduced.
6
+ #
7
+ # To implement a migration you should create a class according to the following contract:
8
+ # module Database
9
+ # class MigrateToVersionXXX
10
+ # def initialize(env)
11
+ # @env = env
12
+ # end
13
+ #
14
+ # def migrate
15
+ # # your migration code here...
16
+ # end
17
+ # end
18
+ # end
19
+ #
20
+ module Sequent
21
+ module Migrations
22
+ class MigrateEvents
23
+
24
+ ##
25
+ # @param env The string representing the current environment. E.g. "development", "production"
26
+ def initialize(env)
27
+ @env = env
28
+ end
29
+
30
+ ##
31
+ #
32
+ # @param current_version The current version of the application. E.g. 10
33
+ # @param new_version The version to migrate to. E.g. 11
34
+ # @param &after_migration_block an optional block to run after the migrations run. E.g. close resources
35
+ #
36
+ def execute_migrations(current_version, new_version, &after_migration_block)
37
+ if current_version != new_version and current_version > 0
38
+ ((current_version + 1)..new_version).each do |upgrade_to_version|
39
+ migration_class = "MigrateToVersion#{upgrade_to_version}".to_sym
40
+ if Kernel.const_defined?(migration_class)
41
+ begin
42
+ Kernel.const_get(migration_class).new(@env).migrate
43
+ ensure
44
+ after_migration_block.call if after_migration_block
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,7 @@
1
+ module Sequent
2
+ module Migrations
3
+
4
+ end
5
+ end
6
+
7
+ require_relative 'migrate_events'
@@ -0,0 +1,3 @@
1
+ require_relative 'core/core'
2
+ require_relative 'migrations/migrations'
3
+ require_relative 'test/test'
@@ -0,0 +1,101 @@
1
+ module Sequent
2
+ module Test
3
+ ##
4
+ # Use in tests
5
+ #
6
+ # This provides a nice DSL for event based testing of your CommandHandler like
7
+ #
8
+ # given_events InvoiceCreatedEvent.new(args)
9
+ # when_command PayInvoiceCommand(args)
10
+ # then_events InvoicePaidEvent(args)
11
+ #
12
+ # Example for Rspec config
13
+ #
14
+ # RSpec.configure do |config|
15
+ # config.include Sequent::Test::CommandHandlerHelpers
16
+ # end
17
+ #
18
+ # Then in a spec
19
+ #
20
+ # describe InvoiceCommandHandler do
21
+ #
22
+ # before :each do
23
+ # @event_store = Sequent::Test::CommandHandlerHelpers::FakeEventStore.new
24
+ # @repository = Sequent::Core::AggregateRepository.new(@event_store)
25
+ # @command_handler = InvoiceCommandHandler.new(@repository)
26
+ # end
27
+ #
28
+ # it "marks an invoice as paid" do
29
+ # given_events InvoiceCreatedEvent.new(args)
30
+ # when_command PayInvoiceCommand(args)
31
+ # then_events InvoicePaidEvent(args)
32
+ # end
33
+ #
34
+ # end
35
+ module CommandHandlerHelpers
36
+
37
+ class FakeEventStore
38
+ def initialize
39
+ @all_events = []
40
+ @stored_events = []
41
+ end
42
+
43
+ def load_events(aggregate_id)
44
+ deserialize_events(@all_events).select { |event| aggregate_id == event.aggregate_id }
45
+ end
46
+
47
+ def stored_events
48
+ deserialize_events(@stored_events)
49
+ end
50
+
51
+ def commit_events(_, events)
52
+ serialized = serialize_events(events)
53
+ @all_events += serialized
54
+ @stored_events += serialized
55
+ end
56
+
57
+ def given_events(events)
58
+ @all_events += serialize_events(events)
59
+ @stored_events = []
60
+ end
61
+
62
+ private
63
+ def serialize_events(events)
64
+ events.map { |event| [event.class.name.to_sym, event.to_json] }
65
+ end
66
+
67
+ def deserialize_events(events)
68
+ events.map do |type, json|
69
+ Class.const_get(type).deserialize_from_json(JSON.parse(json))
70
+ end
71
+ end
72
+
73
+ end
74
+
75
+ def given_events *events
76
+ raise ArgumentError.new("events can not be nil") if events.compact.empty?
77
+ @event_store.given_events(events)
78
+ end
79
+
80
+ def when_command command
81
+ raise "@command_handler is mandatory when using the #{self.class}" unless @command_handler
82
+ raise "Command handler #{@command_handler} cannot handle command #{command}, please configure the command type (forgot an include in the command class?)" unless @command_handler.handles_message?(command)
83
+ @command_handler.handle_message(command)
84
+ @repository.commit(command)
85
+ end
86
+
87
+ def then_events *events
88
+ @event_store.stored_events.map(&:class).should == events.map(&:class)
89
+ @event_store.stored_events.zip(events).each do |actual, expected|
90
+ JSON.parse(actual.payload.to_json).should == JSON.parse(expected.payload.to_json) if expected
91
+ end
92
+ end
93
+
94
+ def then_no_events
95
+ then_events
96
+ end
97
+
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1 @@
1
+ require_relative 'command_handler_helpers'
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Sequent
2
+ VERSION = '0.1.2'
3
+ end