sequent 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
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'