sequent 4.1.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/bin/sequent +2 -1
  3. data/lib/sequent/core/aggregate_repository.rb +31 -0
  4. data/lib/sequent/core/aggregate_root.rb +20 -0
  5. data/lib/sequent/core/command_record.rb +1 -1
  6. data/lib/sequent/core/event_store.rb +32 -1
  7. data/lib/sequent/core/helpers/attr_matchers/argument_serializer.rb +35 -0
  8. data/lib/sequent/core/helpers/attr_matchers/attr_matchers.rb +10 -0
  9. data/lib/sequent/core/helpers/attr_matchers/dsl.rb +23 -0
  10. data/lib/sequent/core/helpers/attr_matchers/equals.rb +24 -0
  11. data/lib/sequent/core/helpers/attr_matchers/greater_than.rb +24 -0
  12. data/lib/sequent/core/helpers/attr_matchers/greater_than_equals.rb +24 -0
  13. data/lib/sequent/core/helpers/attr_matchers/less_than.rb +24 -0
  14. data/lib/sequent/core/helpers/attr_matchers/less_than_equals.rb +24 -0
  15. data/lib/sequent/core/helpers/attr_matchers/not_equals.rb +24 -0
  16. data/lib/sequent/core/helpers/attribute_support.rb +34 -9
  17. data/lib/sequent/core/helpers/autoset_attributes.rb +5 -5
  18. data/lib/sequent/core/helpers/message_dispatcher.rb +20 -0
  19. data/lib/sequent/core/helpers/message_handler.rb +62 -8
  20. data/lib/sequent/core/helpers/message_handler_option_registry.rb +59 -0
  21. data/lib/sequent/core/helpers/message_matchers/any.rb +34 -0
  22. data/lib/sequent/core/helpers/message_matchers/argument_coercer.rb +24 -0
  23. data/lib/sequent/core/helpers/message_matchers/argument_serializer.rb +20 -0
  24. data/lib/sequent/core/helpers/message_matchers/dsl.rb +23 -0
  25. data/lib/sequent/core/helpers/message_matchers/except_opt.rb +24 -0
  26. data/lib/sequent/core/helpers/message_matchers/has_attrs.rb +54 -0
  27. data/lib/sequent/core/helpers/message_matchers/instance_of.rb +24 -0
  28. data/lib/sequent/core/helpers/message_matchers/is_a.rb +34 -0
  29. data/lib/sequent/core/helpers/message_matchers/message_matchers.rb +10 -0
  30. data/lib/sequent/core/helpers/message_router.rb +55 -0
  31. data/lib/sequent/core/projector.rb +28 -0
  32. data/lib/sequent/core/transactions/active_record_transaction_provider.rb +25 -0
  33. data/lib/sequent/core/transactions/read_only_active_record_transaction_provider.rb +46 -0
  34. data/lib/sequent/core/transactions/transactions.rb +1 -0
  35. data/lib/sequent/core/workflow.rb +30 -2
  36. data/lib/sequent/generator/project.rb +7 -0
  37. data/lib/sequent/generator/template_project/ruby-version +1 -0
  38. data/lib/sequent/test/command_handler_helpers.rb +2 -2
  39. data/lib/sequent/util/dry_run.rb +18 -13
  40. data/lib/version.rb +1 -1
  41. metadata +36 -13
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequent
4
+ module Core
5
+ module Helpers
6
+ module MessageMatchers
7
+ class ArgumentCoercer
8
+ class << self
9
+ def coerce_argument(arg)
10
+ fail ArgumentError, 'Cannot coerce nil argument' if arg.nil?
11
+
12
+ return MessageMatchers::InstanceOf.new(arg) if [Class, Module].include?(arg.class)
13
+ return arg if arg.respond_to?(:matches_message?)
14
+
15
+ fail ArgumentError,
16
+ "Can't coerce argument '#{arg}'; " \
17
+ 'must be either a Class, Module or message matcher (respond to :matches_message?)'
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequent
4
+ module Core
5
+ module Helpers
6
+ module MessageMatchers
7
+ class ArgumentSerializer
8
+ class << self
9
+ def serialize_value(value)
10
+ return value.to_s if value.respond_to?(:matches_message?)
11
+ return %("#{value}") if value.is_a?(String)
12
+
13
+ value
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequent
4
+ module Core
5
+ module Helpers
6
+ module MessageMatchers
7
+ module DSL
8
+ def register_matcher(name, matcher_class)
9
+ if respond_to?(name)
10
+ fail ArgumentError, "Cannot register message matcher because it would overwrite existing method '#{name}'"
11
+ end
12
+
13
+ define_method(name) do |*expected|
14
+ matcher_class.new(*expected)
15
+ end
16
+ end
17
+ end
18
+
19
+ extend DSL
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequent
4
+ module Core
5
+ module Helpers
6
+ module MessageMatchers
7
+ module ExceptOpt
8
+ private
9
+
10
+ def excluded?(message)
11
+ [except]
12
+ .flatten
13
+ .compact
14
+ .any? { |x| message.is_a?(x) }
15
+ end
16
+
17
+ def except
18
+ opts.try(:[], :except)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequent
4
+ module Core
5
+ module Helpers
6
+ module MessageMatchers
7
+ HasAttrs = Struct.new(:message_matcher, :expected_attrs) do
8
+ def initialize(message_matcher, expected_attrs)
9
+ super
10
+
11
+ fail ArgumentError, 'Missing required message matcher' if message_matcher.nil?
12
+ fail ArgumentError, 'Missing required expected attrs' if expected_attrs.blank?
13
+
14
+ self.message_matcher = ArgumentCoercer.coerce_argument(message_matcher)
15
+ end
16
+
17
+ def matches_message?(message)
18
+ message_matcher.matches_message?(message) &&
19
+ matches_attrs?(message, expected_attrs)
20
+ end
21
+
22
+ def to_s
23
+ 'has_attrs(' \
24
+ "#{MessageMatchers::ArgumentSerializer.serialize_value(message_matcher)}, " \
25
+ "#{AttrMatchers::ArgumentSerializer.serialize_value(expected_attrs)}" \
26
+ ')'
27
+ end
28
+
29
+ private
30
+
31
+ def matches_attrs?(message, expected_attrs, path = [])
32
+ expected_attrs.all? do |(name, expected_value)|
33
+ matches_attr?(message, expected_value, path.dup.push(name))
34
+ end
35
+ end
36
+
37
+ def matches_attr?(message, expected_value, path)
38
+ return matches_attrs?(message, expected_value, path) if expected_value.is_a?(Hash)
39
+
40
+ expected_value = AttrMatchers::Equals.new(expected_value) unless expected_value.respond_to?(:matches_attr?)
41
+ value = path.reduce(message) { |memo, p| memo.public_send(p) }
42
+
43
+ expected_value.matches_attr?(value)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ Sequent::Core::Helpers::MessageMatchers.register_matcher(
52
+ :has_attrs,
53
+ Sequent::Core::Helpers::MessageMatchers::HasAttrs,
54
+ )
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequent
4
+ module Core
5
+ module Helpers
6
+ module MessageMatchers
7
+ InstanceOf = Struct.new(:expected_class) do
8
+ def matches_message?(message)
9
+ message.instance_of?(expected_class)
10
+ end
11
+
12
+ def to_s
13
+ expected_class
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ Sequent::Core::Helpers::MessageMatchers.register_matcher(
22
+ :instance_of,
23
+ Sequent::Core::Helpers::MessageMatchers::InstanceOf,
24
+ )
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequent
4
+ module Core
5
+ module Helpers
6
+ module MessageMatchers
7
+ IsA = Struct.new(:expected_class, :opts) do
8
+ include ExceptOpt
9
+
10
+ def matches_message?(message)
11
+ message.is_a?(expected_class) unless excluded?(message)
12
+ end
13
+
14
+ def to_s
15
+ "is_a(#{matcher_arguments})"
16
+ end
17
+
18
+ private
19
+
20
+ def matcher_arguments
21
+ arguments = expected_class.to_s
22
+ arguments += ", except: #{except}" if except
23
+ arguments
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ Sequent::Core::Helpers::MessageMatchers.register_matcher(
32
+ :is_a,
33
+ Sequent::Core::Helpers::MessageMatchers::IsA,
34
+ )
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'dsl'
4
+ require_relative 'argument_coercer'
5
+ require_relative 'argument_serializer'
6
+ require_relative 'except_opt'
7
+ require_relative 'instance_of'
8
+ require_relative 'is_a'
9
+ require_relative 'has_attrs'
10
+ require_relative 'any'
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './attr_matchers/attr_matchers'
4
+ require_relative './message_matchers/message_matchers'
5
+
6
+ module Sequent
7
+ module Core
8
+ module Helpers
9
+ class MessageRouter
10
+ attr_reader :routes
11
+
12
+ def initialize
13
+ clear_routes
14
+ end
15
+
16
+ ##
17
+ # Registers a handler for the given matchers.
18
+ #
19
+ # A matcher must implement #matches_message?(message) and return a truthy value when it matches,
20
+ # or a falsey value otherwise.
21
+ #
22
+ def register_matchers(*matchers, handler)
23
+ matchers.each do |matcher|
24
+ @routes[matcher] << handler
25
+ end
26
+ end
27
+
28
+ ##
29
+ # Returns a set of handlers that match the given message, or an empty set when none match.
30
+ #
31
+ def match_message(message)
32
+ @routes
33
+ .reduce(Set.new) do |memo, (matcher, handlers)|
34
+ memo = memo.merge(handlers) if matcher.matches_message?(message)
35
+ memo
36
+ end
37
+ end
38
+
39
+ ##
40
+ # Returns true when there is at least one handler for the given message, or false otherwise.
41
+ #
42
+ def matches_message?(message)
43
+ match_message(message).any?
44
+ end
45
+
46
+ ##
47
+ # Removes all routes from the router.
48
+ #
49
+ def clear_routes
50
+ @routes = Hash.new { |h, k| h[k] = Set.new }
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -92,6 +92,11 @@ module Sequent
92
92
  @persistor = persistor
93
93
  end
94
94
 
95
+ def self.inherited(subclass)
96
+ super
97
+ Projectors << subclass
98
+ end
99
+
95
100
  def self.replay_persistor
96
101
  nil
97
102
  end
@@ -125,5 +130,28 @@ module Sequent
125
130
  end
126
131
  end
127
132
  end
133
+
134
+ #
135
+ # Utility class containing all subclasses of Projector
136
+ #
137
+ class Projectors
138
+ class << self
139
+ def projectors
140
+ @projectors ||= []
141
+ end
142
+
143
+ def all
144
+ projectors
145
+ end
146
+
147
+ def <<(projector)
148
+ projectors << projector
149
+ end
150
+
151
+ def find(projector_name)
152
+ projectors.find { |c| c.name == projector_name }
153
+ end
154
+ end
155
+ end
128
156
  end
129
157
  end
@@ -3,6 +3,31 @@
3
3
  module Sequent
4
4
  module Core
5
5
  module Transactions
6
+ ##
7
+ # Always require a new transaction.
8
+ #
9
+ # Read:
10
+ # http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
11
+ #
12
+ # Without this change, there is a potential bug:
13
+ #
14
+ # ```ruby
15
+ # ActiveRecord::Base.transaction do
16
+ # Sequent.configuration.command_service.execute_commands command
17
+ # end
18
+ #
19
+ # on Command do
20
+ # do.some.things
21
+ # fail ActiveRecord::Rollback
22
+ # end
23
+ # ```
24
+ #
25
+ # In this example, you might be surprised to find that `do.some.things`
26
+ # does not get rolled back! This is because AR doesn't automatically make
27
+ # a "savepoint" for us when we call `.transaction` in a nested manner. In
28
+ # order to enable this behaviour, we have to call `.transaction` like
29
+ # this: `.transaction(requires_new: true)`.
30
+ #
6
31
  class ActiveRecordTransactionProvider
7
32
  def transactional(&block)
8
33
  Sequent::ApplicationRecord.transaction(requires_new: true, &block)
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequent
4
+ module Core
5
+ module Transactions
6
+ class ReadOnlyActiveRecordTransactionProvider
7
+ def initialize(transaction_provider)
8
+ @transaction_provider = transaction_provider
9
+ end
10
+
11
+ def transactional(&block)
12
+ register_call
13
+ @transaction_provider.transactional do
14
+ Sequent::ApplicationRecord.connection.execute('SET TRANSACTION READ ONLY')
15
+ block.call
16
+ rescue ActiveRecord::StatementInvalid
17
+ @skip_set_transaction = true
18
+ raise
19
+ ensure
20
+ deregister_call
21
+ reset_stack_size if stack_size == 0
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def stack_size
28
+ Thread.current[:read_only_active_record_transaction_provider_calls]
29
+ end
30
+
31
+ def register_call
32
+ Thread.current[:read_only_active_record_transaction_provider_calls] ||= 0
33
+ Thread.current[:read_only_active_record_transaction_provider_calls] += 1
34
+ end
35
+
36
+ def deregister_call
37
+ Thread.current[:read_only_active_record_transaction_provider_calls] -= 1
38
+ end
39
+
40
+ def reset_stack_size
41
+ Thread.current[:read_only_active_record_transaction_provider_calls] = nil
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -2,3 +2,4 @@
2
2
 
3
3
  require_relative 'active_record_transaction_provider'
4
4
  require_relative 'no_transactions'
5
+ require_relative 'read_only_active_record_transaction_provider'
@@ -8,7 +8,12 @@ module Sequent
8
8
  class Workflow
9
9
  include Helpers::MessageHandler
10
10
 
11
- def self.on(*message_classes, &block)
11
+ def self.inherited(subclass)
12
+ super
13
+ Workflows << subclass
14
+ end
15
+
16
+ def self.on(*args, **opts, &block)
12
17
  decorated_block = ->(event) do
13
18
  begin
14
19
  old_event = CurrentEvent.current
@@ -18,7 +23,7 @@ module Sequent
18
23
  CurrentEvent.current = old_event
19
24
  end
20
25
  end
21
- super(*message_classes, &decorated_block)
26
+ super(*args, **opts, &decorated_block)
22
27
  end
23
28
 
24
29
  def execute_commands(*commands)
@@ -42,5 +47,28 @@ module Sequent
42
47
  end
43
48
  end
44
49
  end
50
+
51
+ #
52
+ # Utility class containing all subclasses of Workflow
53
+ #
54
+ class Workflows
55
+ class << self
56
+ def workflows
57
+ @workflows ||= []
58
+ end
59
+
60
+ def all
61
+ workflows
62
+ end
63
+
64
+ def <<(workflow)
65
+ workflows << workflow
66
+ end
67
+
68
+ def find(workflow_name)
69
+ workflows.find { |c| c.name == workflow_name }
70
+ end
71
+ end
72
+ end
45
73
  end
46
74
  end
@@ -14,6 +14,7 @@ module Sequent
14
14
  def execute
15
15
  make_directory
16
16
  copy_files
17
+ rename_ruby_version
17
18
  rename_app_file
18
19
  replace_app_name
19
20
  end
@@ -28,6 +29,12 @@ module Sequent
28
29
  FileUtils.copy_entry(File.expand_path('template_project', __dir__), path)
29
30
  end
30
31
 
32
+ # Hidden files are by default excluded from gem build.
33
+ # Therefor we need to rename the ruby-version to .ruby-version.
34
+ def rename_ruby_version
35
+ FileUtils.mv("#{path}/ruby-version", "#{path}/.ruby-version")
36
+ end
37
+
31
38
  def rename_app_file
32
39
  FileUtils.mv("#{path}/my_app.rb", "#{path}/#{name_underscored}.rb")
33
40
  end
@@ -11,8 +11,8 @@ module Sequent
11
11
  # This provides a nice DSL for event based testing of your CommandHandler like
12
12
  #
13
13
  # given_events InvoiceCreatedEvent.new(args)
14
- # when_command PayInvoiceCommand(args)
15
- # then_events InvoicePaidEvent(args)
14
+ # when_command PayInvoiceCommand.new(args)
15
+ # then_events InvoicePaidEvent.new(args)
16
16
  #
17
17
  # Example for Rspec config
18
18
  #
@@ -36,18 +36,18 @@ module Sequent
36
36
 
37
37
  delegate :load_events_for_aggregates,
38
38
  :load_events,
39
- :publish_events,
40
39
  :stream_exists?,
40
+ :events_exists?,
41
41
  to: :event_store
42
42
 
43
- def initialize(result)
44
- @event_store = Sequent::Test::CommandHandlerHelpers::FakeEventStore.new
43
+ def initialize(result, event_store)
44
+ @event_store = event_store
45
45
  @command_with_events = {}
46
46
  @result = result
47
47
  end
48
48
 
49
49
  def commit_events(command, streams_with_events)
50
- event_store.commit_events(command, streams_with_events)
50
+ Sequent.configuration.event_publisher.publish_events(streams_with_events.flat_map { |_, events| events })
51
51
 
52
52
  new_events = streams_with_events.flat_map { |_, events| events }
53
53
  @result.published_command_with_events(command, new_events)
@@ -66,18 +66,18 @@ module Sequent
66
66
  end
67
67
 
68
68
  def process_event(event)
69
- Sequent.configuration.event_handlers.each do |handler|
70
- next unless handler.class.handles_message?(event)
69
+ [*Sequent::Core::Workflows.all, *Sequent::Core::Projectors.all].each do |handler_class|
70
+ next unless handler_class.handles_message?(event)
71
71
 
72
- if handler.is_a?(Sequent::Workflow)
73
- @result.invoked_workflow(EventInvokedHandler.new(event, handler.class))
74
- elsif handler.is_a?(Sequent::Projector)
75
- @result.invoked_projector(EventInvokedHandler.new(event, handler.class))
72
+ if handler_class < Sequent::Workflow
73
+ @result.invoked_workflow(EventInvokedHandler.new(event, handler_class))
74
+ elsif handler_class < Sequent::Projector
75
+ @result.invoked_projector(EventInvokedHandler.new(event, handler_class))
76
76
  else
77
- fail "Unrecognized event_handler #{handler.class} called for event #{event.class}"
77
+ fail "Unrecognized event_handler #{handler_class} called for event #{event.class}"
78
78
  end
79
79
  rescue StandardError
80
- raise PublishEventError.new(handler.class, event)
80
+ raise PublishEventError.new(handler_class, event)
81
81
  end
82
82
  end
83
83
  end
@@ -177,10 +177,14 @@ module Sequent
177
177
  def self.these_commands(commands)
178
178
  current_event_store = Sequent.configuration.event_store
179
179
  current_event_publisher = Sequent.configuration.event_publisher
180
+ current_transaction_provider = Sequent.configuration.transaction_provider
181
+
180
182
  result = Result.new
181
183
 
182
- Sequent.configuration.event_store = EventStoreProxy.new(result)
184
+ Sequent.configuration.event_store = EventStoreProxy.new(result, current_event_store)
183
185
  Sequent.configuration.event_publisher = RecordingEventPublisher.new(result)
186
+ Sequent.configuration.transaction_provider =
187
+ Sequent::Core::Transactions::ReadOnlyActiveRecordTransactionProvider.new(current_transaction_provider)
184
188
 
185
189
  Sequent.command_service.execute_commands(*commands)
186
190
 
@@ -188,6 +192,7 @@ module Sequent
188
192
  ensure
189
193
  Sequent.configuration.event_store = current_event_store
190
194
  Sequent.configuration.event_publisher = current_event_publisher
195
+ Sequent.configuration.transaction_provider = current_transaction_provider
191
196
  end
192
197
  end
193
198
  end
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sequent
4
- VERSION = '4.1.0'
4
+ VERSION = '5.0.0'
5
5
  end