sequent 0.1.5 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 814f54e9a769f6f73fc7c0cab26eebc4b9a191e1
4
- data.tar.gz: c346224df919223cba73db9fe74097c235bacd32
3
+ metadata.gz: 0e8e35986af2a9dcdb19a2d66b291606e852b96c
4
+ data.tar.gz: a049cc85e36f391e22fd35ae3d45061f81eb7562
5
5
  SHA512:
6
- metadata.gz: 4e39adc404ed14163bd5b54b4ebdc558a12d4d56be8b60ad54140ee1cbc9102d89e17359a08d7a37eeeb299b99788359777d9f6cf778362969cb45b3182d493a
7
- data.tar.gz: 667ab8ca0e0b5255184f162210d33a6775b26e952e8868a42a6a8a8ef408b24cec46f118c6fa9400b71a01632a0fb32388e2e23a6ab9362301f2247c2730726a
6
+ metadata.gz: 319d6c1cb5397766edb05bb6239a083d51722490a85f7120c68a4973cda6d30d9a92a2efecaec0c0a0df42d864e80d4142ecc23ceea6255c344076f9703e45f1
7
+ data.tar.gz: e7768f63f1369006ed1a61397ce5d5195e35142a497113ffd6e03c34edffe56f49d20d3271b680b56b18752f3c32f91b8c72f4572fe01cafb4bf3fd16dc6e716
@@ -32,7 +32,6 @@ module Sequent
32
32
 
33
33
  def initialize(event_store)
34
34
  @event_store = event_store
35
- clear
36
35
  end
37
36
 
38
37
  # Adds the given aggregate to the repository (or unit of work).
@@ -57,7 +56,7 @@ module Sequent
57
56
  # Loads aggregate by given id and class
58
57
  # Returns the one in the current Unit Of Work otherwise loads it from history.
59
58
  def load_aggregate(aggregate_id, clazz = nil)
60
- result = aggregates.fetch(aggregate_id) do |aggregate_id|
59
+ result = aggregates.fetch(aggregate_id) do |_|
61
60
  stream, events = @event_store.load_events(aggregate_id)
62
61
  raise AggregateNotFound.new(aggregate_id) unless stream
63
62
  aggregate_class = Class.const_get(stream.aggregate_type)
@@ -69,6 +68,12 @@ module Sequent
69
68
  result
70
69
  end
71
70
 
71
+ ##
72
+ # Returns whether the event store has an aggregate with the given id
73
+ def contains_aggregate?(aggregate_id)
74
+ @event_store.stream_exists?(aggregate_id)
75
+ end
76
+
72
77
  # Gets all uncommitted_events from the 'registered' aggregates
73
78
  # and stores them in the event store.
74
79
  # The command is 'attached' for traceability purpose so we can see
@@ -77,7 +82,7 @@ module Sequent
77
82
  # This is all abstracted away if you use the Sequent::Core::CommandService
78
83
  #
79
84
  def commit(command)
80
- updated_aggregates = aggregates.values.reject {|x| x.uncommitted_events.empty?}
85
+ updated_aggregates = aggregates.values.reject { |x| x.uncommitted_events.empty? }
81
86
  return if updated_aggregates.empty?
82
87
  streams_with_events = updated_aggregates.map do |aggregate|
83
88
  [ aggregate.event_stream, aggregate.uncommitted_events ]
@@ -88,13 +93,13 @@ module Sequent
88
93
 
89
94
  # Clears the Unit of Work.
90
95
  def clear
91
- Thread.current[AGGREGATES_KEY] = {}
96
+ Thread.current[AGGREGATES_KEY] = nil
92
97
  end
93
98
 
94
99
  private
95
100
 
96
101
  def aggregates
97
- Thread.current[AGGREGATES_KEY]
102
+ Thread.current[AGGREGATES_KEY] ||= {}
98
103
  end
99
104
 
100
105
  def store_events(command, streams_with_events)
@@ -88,7 +88,7 @@ module Sequent
88
88
  protected
89
89
 
90
90
  def build_event(event, params = {})
91
- event.new({aggregate_id: @id, sequence_number: @sequence_number}.merge(params))
91
+ event.new(params.merge({aggregate_id: @id, sequence_number: @sequence_number}))
92
92
  end
93
93
 
94
94
  # Provide subclasses nice DSL to 'apply' events via:
@@ -124,7 +124,7 @@ module Sequent
124
124
  protected
125
125
 
126
126
  def build_event(event, params = {})
127
- super(event, {organization_id: @organization_id}.merge(params))
127
+ super(event, params.merge({organization_id: @organization_id}))
128
128
  end
129
129
  end
130
130
  end
@@ -26,6 +26,10 @@ module Sequent
26
26
  @repository = repository
27
27
  end
28
28
 
29
+ def handles_message?(command)
30
+ self.class.message_mapping.keys.include? command.class
31
+ end
32
+
29
33
  protected
30
34
  def do_with_aggregate(command, clazz, aggregate_id = nil)
31
35
  aggregate = @repository.load_aggregate(aggregate_id.nil? ? command.aggregate_id : aggregate_id, clazz)
@@ -47,5 +47,8 @@ module Sequent
47
47
  :find_records, :last_record
48
48
 
49
49
  end
50
+
51
+ # Alias the above class
52
+ Projector = BaseEventHandler
50
53
  end
51
54
  end
@@ -15,13 +15,14 @@ module Sequent
15
15
  class CommandService
16
16
  attr_accessor :configuration
17
17
 
18
- # +DefaultCommandServiceConfiguration+ Configuration class for the CommandService containing:
18
+ # Create a command service with the given configuration.
19
19
  #
20
20
  # +event_store+ The Sequent::Core::EventStore
21
- # +command_handler_classes+ Array of BaseCommandHandler classes that need to handle commands
22
- # +transaction_provider+ How to do transaction management. Defaults to Sequent::Core::Transactions::NoTransactions
23
- # +filters+ List of filter that respond_to :execute(command). Can be useful to do extra checks (security and such).
24
- def initialize(configuration = CommandServiceConfiguration.new)
21
+ # +aggregate_repository+ The Sequent::Core::AggregateRepository
22
+ # +transaction_provider+ How to do transaction management.
23
+ # +command_handlers+ List of command handlers that need to handle commands
24
+ # +command_filters+ List of filter that respond_to :execute(command). Can be useful to do extra checks (security and such).
25
+ def initialize(configuration)
25
26
  self.configuration = configuration
26
27
  end
27
28
 
@@ -15,3 +15,4 @@ require_relative 'tenant_event_store'
15
15
  require_relative 'event_record'
16
16
  require_relative 'command_record'
17
17
  require_relative 'aggregate_snapshotter'
18
+ require_relative 'workflow'
@@ -33,7 +33,8 @@ module Sequent
33
33
  # Returns all events for the aggregate ordered by sequence_number
34
34
  #
35
35
  def load_events(aggregate_id)
36
- stream = stream_record_class.where(aggregate_id: aggregate_id).first!
36
+ stream = stream_record_class.where(aggregate_id: aggregate_id).first
37
+ return nil unless stream
37
38
  events = event_record_class.connection.select_all(%Q{
38
39
  SELECT event_type, event_json
39
40
  FROM #{quote_table_name event_record_class.table_name}
@@ -49,6 +50,10 @@ SELECT event_type, event_json
49
50
  [stream.event_stream, events]
50
51
  end
51
52
 
53
+ def stream_exists?(aggregate_id)
54
+ stream_record_class.exists?(aggregate_id: aggregate_id)
55
+ end
56
+
52
57
  ##
53
58
  # Replays all events in the event store to the registered event_handlers.
54
59
  #
@@ -122,6 +122,10 @@ EOS
122
122
  hash
123
123
  end
124
124
 
125
+ def update(changes)
126
+ self.class.new(attributes.merge(changes))
127
+ end
128
+
125
129
  def validation_errors(prefix = nil)
126
130
  result = errors.to_hash
127
131
  self.class.types.each do |field|
@@ -11,7 +11,7 @@ module Sequent
11
11
  # end
12
12
  #
13
13
  # You typically do not need to include this module in your classes. If you extend from
14
- # Sequent::Core::AggregateRoot, Sequent::Core::BaseEventHandler or Sequent::Core::BaseCommandHandler
14
+ # Sequent::Core::AggregateRoot, Sequent::Core::Projector, Sequent::Core::Workflow or Sequent::Core::BaseCommandHandler
15
15
  # you will get this functionality for free.
16
16
  #
17
17
  module SelfApplier
@@ -18,7 +18,7 @@ module Sequent
18
18
  #
19
19
  # Example:
20
20
  #
21
- # class InvoiceEventHandler < Sequent::Core::BaseEventHandler
21
+ # class InvoiceEventHandler < Sequent::Core::Projector
22
22
  # on RecipientMovedEvent do |event|
23
23
  # update_all_records InvoiceRecord, recipient_id: event.recipient.aggregate_id do |record|
24
24
  # record.recipient_street = record.recipient.street
@@ -0,0 +1,13 @@
1
+ require_relative 'helpers/self_applier'
2
+
3
+ module Sequent
4
+ module Core
5
+ class Workflow
6
+ include Helpers::SelfApplier
7
+
8
+ def execute_commands(*commands)
9
+ Sequent.configuration.command_service.execute_commands(*commands)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,6 +1,5 @@
1
1
  require_relative 'core/core'
2
2
  require_relative 'migrations/migrations'
3
- require_relative 'test/test'
4
3
  require_relative 'configuration'
5
4
 
6
5
  require 'logger'
@@ -0,0 +1,4 @@
1
+ require_relative 'sequent'
2
+ require_relative 'test/command_handler_helpers'
3
+ require_relative 'test/event_stream_helpers'
4
+ require_relative 'test/event_handler_helpers'
@@ -33,6 +33,11 @@ module Sequent
33
33
  # then_events InvoicePaidEvent(args)
34
34
  # end
35
35
  #
36
+ # it "rejects coupon when does not exist" do
37
+ # given_events CartCreatedEvent.new(args)
38
+ # when_command ApplyDiscountCouponCommand(args)
39
+ # then_fails_with CouponDoesNotExist
40
+ # end
36
41
  # end
37
42
  module CommandHandlerHelpers
38
43
 
@@ -72,6 +77,10 @@ module Sequent
72
77
  @stored_events = []
73
78
  end
74
79
 
80
+ def stream_exists?(aggregate_id)
81
+ @event_streams.has_key?(aggregate_id)
82
+ end
83
+
75
84
  private
76
85
 
77
86
  def to_event_streams(events)
@@ -107,23 +116,32 @@ module Sequent
107
116
  Class.const_get(type).deserialize_from_json(Sequent::Core::Oj.strict_load(json))
108
117
  end
109
118
  end
110
-
111
119
  end
112
120
 
113
121
  def given_events *events
114
- @event_store.given_events(events)
122
+ @event_store.given_events(events.flatten(1))
115
123
  end
116
124
 
117
125
  def when_command command
118
126
  raise "@command_handler is mandatory when using the #{self.class}" unless @command_handler
119
127
  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)
120
- @command_handler.handle_message(command)
128
+ begin
129
+ @command_handler.handle_message(command)
130
+ rescue => e
131
+ @unhandled_error = e
132
+ end
121
133
  @repository.commit(command)
134
+ @repository.clear
135
+ end
136
+
137
+ def then_fails_with clazz
138
+ expect(@unhandled_error).to be_kind_of(clazz)
139
+ @unhandled_error = nil
122
140
  end
123
141
 
124
142
  def then_events *events
125
- expect(@event_store.stored_events.map(&:class)).to eq(events.map(&:class))
126
- @event_store.stored_events.zip(events).each do |actual, expected|
143
+ expect(@event_store.stored_events.map(&:class)).to eq(events.flatten(1).map(&:class))
144
+ @event_store.stored_events.zip(events.flatten(1)).each do |actual, expected|
127
145
  expect(Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(actual.payload))).to eq(Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(expected.payload))) if expected
128
146
  end
129
147
  end
@@ -132,7 +150,11 @@ module Sequent
132
150
  then_events
133
151
  end
134
152
 
153
+ def self.included(spec)
154
+ spec.after do
155
+ raise @unhandled_error if @unhandled_error
156
+ end
157
+ end
135
158
  end
136
-
137
159
  end
138
160
  end
@@ -0,0 +1,71 @@
1
+ module Sequent
2
+ module Test
3
+ ##
4
+ # Use in tests
5
+ #
6
+ # This provides a nice DSL for testing your event handlers.
7
+ # E.g.
8
+ #
9
+ # when_event UserWasRegistered.new(args)
10
+ # then_commands SendWelcomeEmail.new(args)
11
+ #
12
+ # Example for Rspec config
13
+ #
14
+ # RSpec.configure do |config|
15
+ # config.include Sequent::Test::WorkflowHelpers
16
+ # end
17
+ #
18
+ # Then in a spec
19
+ #
20
+ # describe SendWelcomeMailWorkflow do
21
+ # let(:workflow) { SendWelcomeMailWorkflow.new }
22
+ #
23
+ # it "sends a welcome mail" do
24
+ # when_event UserWasRegistered.new(args)
25
+ # then_commands SendWelcomeEmail.new(args)
26
+ # end
27
+ # end
28
+ module WorkflowHelpers
29
+
30
+ class FakeCommandService
31
+ attr_reader :recorded_commands
32
+
33
+ def initialize
34
+ @recorded_commands = []
35
+ end
36
+
37
+ def execute_commands(*commands)
38
+ @recorded_commands += commands
39
+ end
40
+ end
41
+
42
+ def then_events *events
43
+ expect(@event_store.stored_events.map(&:class)).to eq(events.flatten(1).map(&:class))
44
+ @event_store.stored_events.zip(events.flatten(1)).each do |actual, expected|
45
+ expect(Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(actual.payload))).to eq(Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(expected.payload))) if expected
46
+ end
47
+ end
48
+
49
+ def then_no_events
50
+ then_events
51
+ end
52
+
53
+ def when_event(event)
54
+ workflow.handle_message event
55
+ end
56
+
57
+ def then_commands(*commands)
58
+ recorded = fake_command_service.recorded_commands
59
+ expect(recorded.map(&:class)).to eq(commands.flatten(1).map(&:class))
60
+ expect(fake_command_service.recorded_commands).to eq(commands.flatten(1))
61
+ end
62
+
63
+ def self.included(spec)
64
+ spec.let(:fake_command_service) { FakeCommandService.new }
65
+ spec.before do
66
+ Sequent.configure { |c| c.command_service = fake_command_service }
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,78 @@
1
+ module Sequent
2
+ module Test
3
+ ##
4
+ # Use in tests
5
+ #
6
+ # This provides a nice DSL for generating streams of events. FactoryGirl is required when using this helper.
7
+ #
8
+ # Example for Rspec config
9
+ #
10
+ # RSpec.configure do |config|
11
+ # config.include Sequent::Test::EventStreamHelpers
12
+ # end
13
+ #
14
+ # Then in a spec
15
+ #
16
+ # given_stream_for(aggregate_id: 'X') do |s|
17
+ # s.group_created owner_aggregate_id: 'Y'
18
+ # s.group_opened
19
+ # s.owner_joined_group owner_aggregate_id: 'Y'
20
+ # end
21
+ #
22
+ # Methods on `s` will be FactoryGirl factories. All arguments will be passed on to FactoryGirl's build method.
23
+ # Aggregate ids and sequence numbers will be set automatically.
24
+ #
25
+ # The example above can also be written as follows:
26
+ #
27
+ # events = event_stream(aggregate_id: 'X') do |s|
28
+ # s.group_created owner_aggregate_id: 'Y'
29
+ # s.group_opened
30
+ # s.owner_joined_group owner_aggregate_id: 'Y'
31
+ # end
32
+ #
33
+ # given_events(events)
34
+ #
35
+ module EventStreamHelpers
36
+ class Builder
37
+ attr_reader :events
38
+
39
+ def initialize(aggregate_id)
40
+ @aggregate_id = aggregate_id
41
+ @events = []
42
+ end
43
+
44
+ def method_missing(name, *args, &block)
45
+ args = prepare_arguments(args)
46
+ @events << FactoryGirl.build(name, *args, &block)
47
+ end
48
+
49
+ private
50
+
51
+ def prepare_arguments(args)
52
+ options = args.last.is_a?(Hash) ? args.pop : {}
53
+ args << options.merge(aggregate_id: @aggregate_id, sequence_number: next_sequence_number)
54
+ end
55
+
56
+ def next_sequence_number
57
+ @events.count + 1
58
+ end
59
+ end
60
+
61
+ def event_stream(aggregate_id:, &block)
62
+ builder = Builder.new(aggregate_id)
63
+ block.call(builder)
64
+ builder.events
65
+ end
66
+
67
+ def given_stream_for(aggregate_id:, &block)
68
+ given_events(*event_stream(aggregate_id: aggregate_id, &block))
69
+ end
70
+
71
+ def self.included(spec)
72
+ require 'factory_girl'
73
+ rescue LoadError
74
+ raise ArgumentError, "Factory girl is required to use the event stream helpers"
75
+ end
76
+ end
77
+ end
78
+ end
@@ -1,3 +1,3 @@
1
1
  module Sequent
2
- VERSION = '0.1.5'
2
+ VERSION = '0.1.6'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lars Vonk
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2015-06-25 00:00:00.000000000 Z
13
+ date: 2015-08-03 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -205,11 +205,14 @@ files:
205
205
  - lib/sequent/core/transactions/no_transactions.rb
206
206
  - lib/sequent/core/transactions/transactions.rb
207
207
  - lib/sequent/core/value_object.rb
208
+ - lib/sequent/core/workflow.rb
208
209
  - lib/sequent/migrations/migrate_events.rb
209
210
  - lib/sequent/migrations/migrations.rb
210
211
  - lib/sequent/sequent.rb
212
+ - lib/sequent/test.rb
211
213
  - lib/sequent/test/command_handler_helpers.rb
212
- - lib/sequent/test/test.rb
214
+ - lib/sequent/test/event_handler_helpers.rb
215
+ - lib/sequent/test/event_stream_helpers.rb
213
216
  - lib/version.rb
214
217
  homepage: https://github.com/zilverline/sequent
215
218
  licenses:
@@ -231,7 +234,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
231
234
  version: '0'
232
235
  requirements: []
233
236
  rubyforge_project:
234
- rubygems_version: 2.2.2
237
+ rubygems_version: 2.4.5
235
238
  signing_key:
236
239
  specification_version: 4
237
240
  summary: Event sourcing framework for Ruby
@@ -1 +0,0 @@
1
- require_relative 'command_handler_helpers'