sequent 4.1.0 → 5.0.0

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 (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