sequent 3.3.1 → 4.1.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/bin/sequent +31 -25
  3. data/lib/notices.rb +6 -0
  4. data/lib/sequent/application_record.rb +2 -0
  5. data/lib/sequent/configuration.rb +29 -29
  6. data/lib/sequent/core/aggregate_repository.rb +24 -14
  7. data/lib/sequent/core/aggregate_root.rb +16 -7
  8. data/lib/sequent/core/aggregate_roots.rb +24 -0
  9. data/lib/sequent/core/aggregate_snapshotter.rb +8 -5
  10. data/lib/sequent/core/base_command_handler.rb +4 -2
  11. data/lib/sequent/core/command.rb +30 -11
  12. data/lib/sequent/core/command_record.rb +12 -4
  13. data/lib/sequent/core/command_service.rb +41 -25
  14. data/lib/sequent/core/core.rb +2 -0
  15. data/lib/sequent/core/current_event.rb +2 -0
  16. data/lib/sequent/core/event.rb +16 -11
  17. data/lib/sequent/core/event_publisher.rb +20 -15
  18. data/lib/sequent/core/event_record.rb +7 -7
  19. data/lib/sequent/core/event_store.rb +75 -49
  20. data/lib/sequent/core/ext/ext.rb +9 -1
  21. data/lib/sequent/core/helpers/array_with_type.rb +4 -1
  22. data/lib/sequent/core/helpers/association_validator.rb +9 -7
  23. data/lib/sequent/core/helpers/attribute_support.rb +64 -33
  24. data/lib/sequent/core/helpers/autoset_attributes.rb +4 -4
  25. data/lib/sequent/core/helpers/boolean_validator.rb +6 -1
  26. data/lib/sequent/core/helpers/copyable.rb +2 -2
  27. data/lib/sequent/core/helpers/date_time_validator.rb +4 -1
  28. data/lib/sequent/core/helpers/date_validator.rb +6 -1
  29. data/lib/sequent/core/helpers/default_validators.rb +12 -10
  30. data/lib/sequent/core/helpers/equal_support.rb +8 -6
  31. data/lib/sequent/core/helpers/helpers.rb +2 -0
  32. data/lib/sequent/core/helpers/mergable.rb +6 -4
  33. data/lib/sequent/core/helpers/message_handler.rb +3 -1
  34. data/lib/sequent/core/helpers/param_support.rb +19 -15
  35. data/lib/sequent/core/helpers/secret.rb +14 -12
  36. data/lib/sequent/core/helpers/string_support.rb +5 -4
  37. data/lib/sequent/core/helpers/string_to_value_parsers.rb +7 -2
  38. data/lib/sequent/core/helpers/string_validator.rb +6 -1
  39. data/lib/sequent/core/helpers/type_conversion_support.rb +5 -3
  40. data/lib/sequent/core/helpers/uuid_helper.rb +5 -2
  41. data/lib/sequent/core/helpers/value_validators.rb +23 -9
  42. data/lib/sequent/core/persistors/active_record_persistor.rb +19 -9
  43. data/lib/sequent/core/persistors/persistor.rb +16 -14
  44. data/lib/sequent/core/persistors/persistors.rb +2 -0
  45. data/lib/sequent/core/persistors/replay_optimized_postgres_persistor.rb +70 -47
  46. data/lib/sequent/core/projector.rb +25 -22
  47. data/lib/sequent/core/random_uuid_generator.rb +2 -0
  48. data/lib/sequent/core/sequent_oj.rb +2 -0
  49. data/lib/sequent/core/stream_record.rb +9 -3
  50. data/lib/sequent/core/transactions/active_record_transaction_provider.rb +7 -9
  51. data/lib/sequent/core/transactions/no_transactions.rb +2 -1
  52. data/lib/sequent/core/transactions/transactions.rb +2 -0
  53. data/lib/sequent/core/value_object.rb +8 -10
  54. data/lib/sequent/core/workflow.rb +7 -5
  55. data/lib/sequent/generator/aggregate.rb +16 -10
  56. data/lib/sequent/generator/command.rb +26 -19
  57. data/lib/sequent/generator/event.rb +19 -17
  58. data/lib/sequent/generator/generator.rb +6 -0
  59. data/lib/sequent/generator/project.rb +3 -1
  60. data/lib/sequent/generator/template_project/Gemfile +1 -1
  61. data/lib/sequent/generator/template_project/spec/app/projectors/post_projector_spec.rb +1 -1
  62. data/lib/sequent/generator/template_project/spec/lib/post/post_command_handler_spec.rb +1 -1
  63. data/lib/sequent/generator.rb +3 -4
  64. data/lib/sequent/migrations/executor.rb +30 -9
  65. data/lib/sequent/migrations/functions.rb +5 -6
  66. data/lib/sequent/migrations/migrate_events.rb +12 -9
  67. data/lib/sequent/migrations/migrations.rb +2 -1
  68. data/lib/sequent/migrations/planner.rb +33 -23
  69. data/lib/sequent/migrations/projectors.rb +4 -3
  70. data/lib/sequent/migrations/sql.rb +2 -0
  71. data/lib/sequent/migrations/view_schema.rb +93 -44
  72. data/lib/sequent/rake/migration_tasks.rb +59 -23
  73. data/lib/sequent/rake/tasks.rb +5 -2
  74. data/lib/sequent/sequent.rb +6 -1
  75. data/lib/sequent/support/database.rb +39 -17
  76. data/lib/sequent/support/view_projection.rb +6 -3
  77. data/lib/sequent/support/view_schema.rb +2 -0
  78. data/lib/sequent/support.rb +2 -0
  79. data/lib/sequent/test/command_handler_helpers.rb +39 -17
  80. data/lib/sequent/test/event_handler_helpers.rb +10 -4
  81. data/lib/sequent/test/event_stream_helpers.rb +7 -3
  82. data/lib/sequent/test/time_comparison.rb +12 -5
  83. data/lib/sequent/test.rb +2 -0
  84. data/lib/sequent/util/dry_run.rb +194 -0
  85. data/lib/sequent/util/printer.rb +6 -5
  86. data/lib/sequent/util/skip_if_already_processing.rb +21 -5
  87. data/lib/sequent/util/timer.rb +2 -0
  88. data/lib/sequent/util/util.rb +3 -0
  89. data/lib/sequent.rb +4 -0
  90. data/lib/version.rb +3 -1
  91. metadata +110 -59
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/hash_with_indifferent_access'
2
4
 
3
5
  module Sequent
@@ -16,20 +18,25 @@ module Sequent
16
18
  end
17
19
 
18
20
  def self.read_config(env)
19
- fail ArgumentError.new("env is mandatory") unless env
21
+ fail ArgumentError, 'env is mandatory' unless env
20
22
 
21
23
  database_yml = File.join(Sequent.configuration.database_config_directory, 'database.yml')
22
- YAML.load(ERB.new(File.read(database_yml)).result)[env]
24
+ config = YAML.safe_load(ERB.new(File.read(database_yml)).result, aliases: true)[env]
25
+ if Gem.loaded_specs['activerecord'].version >= Gem::Version.create('6.1.0')
26
+ ActiveRecord::Base.configurations.resolve(config).configuration_hash.with_indifferent_access
27
+ else
28
+ ActiveRecord::Base.resolve_config_for_connection(config)
29
+ end
23
30
  end
24
31
 
25
32
  def self.create!(db_config)
26
- ActiveRecord::Base.establish_connection(db_config.merge('database' => 'postgres'))
27
- ActiveRecord::Base.connection.create_database(db_config['database'])
33
+ ActiveRecord::Base.establish_connection(db_config.merge(database: 'postgres'))
34
+ ActiveRecord::Base.connection.create_database(db_config[:database])
28
35
  end
29
36
 
30
37
  def self.drop!(db_config)
31
- ActiveRecord::Base.establish_connection(db_config.merge('database' => 'postgres'))
32
- ActiveRecord::Base.connection.drop_database(db_config['database'])
38
+ ActiveRecord::Base.establish_connection(db_config.merge(database: 'postgres'))
39
+ ActiveRecord::Base.connection.drop_database(db_config[:database])
33
40
  end
34
41
 
35
42
  def self.establish_connection(db_config)
@@ -46,9 +53,8 @@ module Sequent
46
53
 
47
54
  def self.create_schema(schema)
48
55
  sql = "CREATE SCHEMA IF NOT EXISTS #{schema}"
49
- if user = ActiveRecord::Base.connection_config[:username]
50
- sql += " AUTHORIZATION #{user}"
51
- end
56
+ user = configuration_hash[:username]
57
+ sql += %( AUTHORIZATION "#{user}") if user
52
58
  execute_sql(sql)
53
59
  end
54
60
 
@@ -57,27 +63,41 @@ module Sequent
57
63
  end
58
64
 
59
65
  def self.with_schema_search_path(search_path, db_config, env = ENV['RACK_ENV'])
60
- fail ArgumentError.new("env is required") unless env
66
+ fail ArgumentError, 'env is required' unless env
61
67
 
62
68
  disconnect!
63
- original_search_paths = db_config['schema_search_path'].dup
64
- ActiveRecord::Base.configurations[env.to_s] = ActiveSupport::HashWithIndifferentAccess.new(db_config).stringify_keys
65
- db_config['schema_search_path'] = search_path
69
+ original_search_paths = db_config[:schema_search_path].dup
70
+
71
+ if ActiveRecord::VERSION::MAJOR < 6
72
+ ActiveRecord::Base.configurations[env.to_s] =
73
+ ActiveSupport::HashWithIndifferentAccess.new(db_config).stringify_keys
74
+ end
75
+
76
+ db_config[:schema_search_path] = search_path
77
+
66
78
  ActiveRecord::Base.establish_connection db_config
67
79
 
68
80
  yield
69
81
  ensure
70
82
  disconnect!
71
- db_config['schema_search_path'] = original_search_paths
83
+ db_config[:schema_search_path] = original_search_paths
72
84
  establish_connection(db_config)
73
85
  end
74
86
 
75
87
  def self.schema_exists?(schema)
76
88
  ActiveRecord::Base.connection.execute(
77
- "SELECT schema_name FROM information_schema.schemata WHERE schema_name like '#{schema}'"
89
+ "SELECT schema_name FROM information_schema.schemata WHERE schema_name like '#{schema}'",
78
90
  ).count == 1
79
91
  end
80
92
 
93
+ def self.configuration_hash
94
+ if Gem.loaded_specs['activesupport'].version >= Gem::Version.create('6.1.0')
95
+ ActiveRecord::Base.connection_db_config.configuration_hash
96
+ else
97
+ ActiveRecord::Base.connection_config
98
+ end
99
+ end
100
+
81
101
  def schema_exists?(schema)
82
102
  self.class.schema_exists?(schema)
83
103
  end
@@ -94,9 +114,11 @@ module Sequent
94
114
  self.class.execute_sql(sql)
95
115
  end
96
116
 
97
- def migrate(migrations_path, verbose: true)
117
+ def migrate(migrations_path, schema_migration: ActiveRecord::SchemaMigration, verbose: true)
98
118
  ActiveRecord::Migration.verbose = verbose
99
- if ActiveRecord::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR >= 2
119
+ if ActiveRecord::VERSION::MAJOR >= 6
120
+ ActiveRecord::MigrationContext.new([migrations_path], schema_migration).up
121
+ elsif ActiveRecord::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR >= 2
100
122
  ActiveRecord::MigrationContext.new([migrations_path]).up
101
123
  else
102
124
  ActiveRecord::Migrator.migrate(migrations_path)
@@ -1,9 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'postgresql_cursor'
2
4
 
3
5
  module Sequent
4
6
  module Support
5
7
  class ViewProjection
6
8
  attr_reader :name, :version, :schema_definition
9
+
7
10
  def initialize(options)
8
11
  @name = options.fetch(:name)
9
12
  @version = options.fetch(:version)
@@ -20,7 +23,7 @@ module Sequent
20
23
  ordering = Events::ORDERED_BY_STREAM
21
24
  event_store.replay_events_from_cursor(
22
25
  block_size: 10_000,
23
- get_events: ->() { ordering[event_store] }
26
+ get_events: -> { ordering[event_store] },
24
27
  )
25
28
  end
26
29
  end
@@ -42,13 +45,13 @@ module Sequent
42
45
  module Events
43
46
  extend ActiveRecord::ConnectionAdapters::Quoting
44
47
 
45
- ORDERED_BY_STREAM = lambda do |event_store|
48
+ ORDERED_BY_STREAM = ->(_event_store) do
46
49
  event_records = quote_table_name(Sequent.configuration.event_record_class.table_name)
47
50
  stream_records = quote_table_name(Sequent.configuration.stream_record_class.table_name)
48
51
  snapshot_event_type = quote(Sequent.configuration.snapshot_event_class)
49
52
 
50
53
  Sequent.configuration.event_record_class
51
- .select("event_type, event_json")
54
+ .select('event_type, event_json')
52
55
  .joins("INNER JOIN #{stream_records} ON #{event_records}.stream_record_id = #{stream_records}.id")
53
56
  .where("event_type <> #{snapshot_event_type}")
54
57
  .order!("#{stream_records}.id, #{event_records}.sequence_number")
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Support
3
5
  class ViewSchema < ActiveRecord::Schema
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'support/database'
2
4
  require_relative 'support/view_projection'
3
5
  require_relative 'support/view_schema'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thread_safe'
2
4
  require 'sequent/core/event_store'
3
5
 
@@ -36,7 +38,6 @@ module Sequent
36
38
  #
37
39
  # end
38
40
  module CommandHandlerHelpers
39
-
40
41
  class FakeEventStore
41
42
  extend Forwardable
42
43
 
@@ -89,21 +90,31 @@ module Sequent
89
90
  end
90
91
 
91
92
  def stream_exists?(aggregate_id)
92
- @event_streams.has_key?(aggregate_id)
93
+ @event_streams.key?(aggregate_id)
94
+ end
95
+
96
+ def events_exists?(aggregate_id)
97
+ @event_streams[aggregate_id].present?
93
98
  end
94
99
 
95
100
  private
96
101
 
97
102
  def to_event_streams(events)
98
- # Specs use a simple list of given events. We need a mapping from StreamRecord to the associated events for the event store.
103
+ # Specs use a simple list of given events.
104
+ # We need a mapping from StreamRecord to the associated events for the event store.
99
105
  streams_by_aggregate_id = {}
100
106
  events.map do |event|
101
107
  event_stream = streams_by_aggregate_id.fetch(event.aggregate_id) do |aggregate_id|
102
108
  streams_by_aggregate_id[aggregate_id] =
103
109
  find_event_stream(aggregate_id) ||
104
110
  begin
105
- aggregate_type = FakeEventStore.aggregate_type_for_event(event)
106
- raise "cannot find aggregate type associated with creation event #{event}, did you include an event handler in your aggregate for this event?" unless aggregate_type
111
+ aggregate_type = aggregate_type_for_event(event)
112
+ unless aggregate_type
113
+ fail <<~EOS
114
+ Cannot find aggregate type associated with creation event #{event}, did you include an event handler in your aggregate for this event?
115
+ EOS
116
+ end
117
+
107
118
  Sequent::Core::EventStream.new(aggregate_type: aggregate_type.name, aggregate_id: aggregate_id)
108
119
  end
109
120
  end
@@ -111,10 +122,10 @@ module Sequent
111
122
  end
112
123
  end
113
124
 
114
- def self.aggregate_type_for_event(event)
125
+ def aggregate_type_for_event(event)
115
126
  @event_to_aggregate_type ||= ThreadSafe::Cache.new
116
127
  @event_to_aggregate_type.fetch_or_store(event.class) do |klass|
117
- Sequent::Core::AggregateRoot.descendants.find { |x| x.message_mapping.has_key?(klass) }
128
+ Sequent::Core::AggregateRoots.all.find { |x| x.message_mapping.key?(klass) }
118
129
  end
119
130
  end
120
131
 
@@ -129,30 +140,41 @@ module Sequent
129
140
  end
130
141
  end
131
142
 
132
- def given_events *events
143
+ def given_events(*events)
133
144
  Sequent.configuration.event_store.given_events(events.flatten(1))
134
145
  end
135
146
 
136
- def when_command command
147
+ def when_command(command)
137
148
  Sequent.configuration.command_service.execute_commands command
138
149
  end
139
150
 
140
151
  def then_events(*expected_events)
141
- expected_classes = expected_events.flatten(1).map { |event| event.class == Class ? event : event.class }
152
+ expected_classes = expected_events.flatten(1).map { |event| event.instance_of?(Class) ? event : event.class }
142
153
  expect(Sequent.configuration.event_store.stored_events.map(&:class)).to eq(expected_classes)
143
154
 
144
- Sequent.configuration.event_store.stored_events.zip(expected_events.flatten(1)).each_with_index do |(actual, expected), index|
145
- next if expected.class == Class
146
- _actual = Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(actual.payload))
147
- _expected = Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(expected.payload))
148
- expect(_actual).to eq(_expected), "#{index+1}th Event of type #{actual.class} not equal\nexpected: #{_expected.inspect}\n got: #{_actual.inspect}" if expected
149
- end
155
+ Sequent
156
+ .configuration
157
+ .event_store
158
+ .stored_events
159
+ .zip(expected_events.flatten(1))
160
+ .each_with_index do |(actual, expected), index|
161
+ next if expected.instance_of?(Class)
162
+
163
+ actual_hash = Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(actual.payload))
164
+ expected_hash = Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(expected.payload))
165
+ next unless expected
166
+
167
+ # rubocop:disable Layout/LineLength
168
+ expect(actual_hash)
169
+ .to eq(expected_hash),
170
+ "#{index + 1}th Event of type #{actual.class} not equal\nexpected: #{expected_hash.inspect}\n got: #{actual_hash.inspect}"
171
+ # rubocop:enable Layout/LineLength
172
+ end
150
173
  end
151
174
 
152
175
  def then_no_events
153
176
  then_events
154
177
  end
155
-
156
178
  end
157
179
  end
158
180
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Test
3
5
  ##
@@ -26,7 +28,6 @@ module Sequent
26
28
  # end
27
29
  # end
28
30
  module WorkflowHelpers
29
-
30
31
  class FakeTransactionProvider
31
32
  def initialize
32
33
  @after_commit_blocks = []
@@ -55,12 +56,17 @@ module Sequent
55
56
  end
56
57
 
57
58
  def then_events(*expected_events)
58
- expected_classes = expected_events.flatten(1).map { |event| event.class == Class ? event : event.class }
59
+ expected_classes = expected_events.flatten(1).map { |event| event.instance_of?(Class) ? event : event.class }
59
60
  expect(Sequent.configuration.event_store.stored_events.map(&:class)).to eq(expected_classes)
60
61
 
61
62
  Sequent.configuration.event_store.stored_events.zip(expected_events.flatten(1)).each do |actual, expected|
62
- next if expected.class == Class
63
- 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
63
+ next if expected.instance_of?(Class)
64
+
65
+ next unless expected
66
+
67
+ expect(
68
+ Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(actual.payload)),
69
+ ).to eq(Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(expected.payload)))
64
70
  end
65
71
  end
66
72
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Test
3
5
  ##
@@ -41,12 +43,14 @@ module Sequent
41
43
  @events = []
42
44
  end
43
45
 
46
+ # rubocop:disable Style/MissingRespondToMissing
44
47
  def method_missing(name, *args, &block)
45
48
  args = prepare_arguments(args)
46
49
  @events << FactoryBot.build(name, *args, &block)
47
50
  end
51
+ # rubocop:enable Style/MissingRespondToMissing
48
52
 
49
- private
53
+ private
50
54
 
51
55
  def prepare_arguments(args)
52
56
  options = args.last.is_a?(Hash) ? args.pop : {}
@@ -68,10 +72,10 @@ module Sequent
68
72
  given_events(*event_stream(aggregate_id: aggregate_id, &block))
69
73
  end
70
74
 
71
- def self.included(spec)
75
+ def self.included(_spec)
72
76
  require 'factory_bot'
73
77
  rescue LoadError
74
- raise ArgumentError, "Factory bot is required to use the event stream helpers"
78
+ raise ArgumentError, 'Factory bot is required to use the event stream helpers'
75
79
  end
76
80
  end
77
81
  end
@@ -1,20 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Test
3
5
  module DateTimePatches
4
6
  module Normalize
5
7
  def normalize
6
- in_time_zone("UTC")
8
+ in_time_zone('UTC')
7
9
  end
8
10
  end
9
11
 
10
12
  module Compare
11
- alias_method :'___<=>', :<=>
13
+ # rubocop:disable Style/Alias
14
+ alias :'___<=>' :'<=>'
15
+ # rubocop:enable Style/Alias
12
16
 
13
17
  # omit nsec in datetime comparisons
14
18
  def <=>(other)
15
- if other && other.is_a?(DateTimePatches::Normalize)
19
+ if other&.is_a?(DateTimePatches::Normalize)
16
20
  result = normalize.iso8601 <=> other.normalize.iso8601
17
21
  return result unless result == 0
22
+
18
23
  # use usec here, which *truncates* the nsec (ie. like Postgres)
19
24
  return normalize.usec <=> other.normalize.usec
20
25
  end
@@ -35,6 +40,8 @@ class DateTime
35
40
  prepend Sequent::Test::DateTimePatches::Compare
36
41
  end
37
42
 
38
- class ActiveSupport::TimeWithZone
39
- prepend Sequent::Test::DateTimePatches::Normalize
43
+ module ActiveSupport
44
+ class TimeWithZone
45
+ prepend Sequent::Test::DateTimePatches::Normalize
46
+ end
40
47
  end
data/lib/sequent/test.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'sequent'
2
4
  require_relative 'test/command_handler_helpers'
3
5
  require_relative 'test/event_stream_helpers'
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test/command_handler_helpers'
4
+
5
+ module Sequent
6
+ module Util
7
+ ##
8
+ # Dry run provides the ability to inspect what would
9
+ # happen if the given commands would be executed
10
+ # without actually committing the results.
11
+ # You can inspect which commands are executed
12
+ # and what the resulting events would be
13
+ # with theSequent::Projector's and Sequent::Workflow's
14
+ # that would be invoked (without actually invoking them).
15
+ #
16
+ # Since the Workflow's are not actually invoked new commands
17
+ # resulting from this Workflow will of course not be in the result.
18
+ #
19
+ # Caution: Since the Sequent Configuration is shared between threads this method
20
+ # is not Thread safe.
21
+ #
22
+ # Example usage:
23
+ #
24
+ # result = Sequent.dry_run(create_foo_command, ping_foo_command)
25
+ #
26
+ # result.print(STDOUT)
27
+ #
28
+ module DryRun
29
+ EventInvokedHandler = Struct.new(:event, :handler)
30
+
31
+ ##
32
+ # Proxies the given EventStore implements commit_events
33
+ # that instead of publish and store just publishes the events.
34
+ class EventStoreProxy
35
+ attr_reader :command_with_events, :event_store
36
+
37
+ delegate :load_events_for_aggregates,
38
+ :load_events,
39
+ :publish_events,
40
+ :stream_exists?,
41
+ to: :event_store
42
+
43
+ def initialize(result)
44
+ @event_store = Sequent::Test::CommandHandlerHelpers::FakeEventStore.new
45
+ @command_with_events = {}
46
+ @result = result
47
+ end
48
+
49
+ def commit_events(command, streams_with_events)
50
+ event_store.commit_events(command, streams_with_events)
51
+
52
+ new_events = streams_with_events.flat_map { |_, events| events }
53
+ @result.published_command_with_events(command, new_events)
54
+ end
55
+ end
56
+
57
+ ##
58
+ # Records which Projector's and Workflow's are executed
59
+ #
60
+ class RecordingEventPublisher < Sequent::Core::EventPublisher
61
+ attr_reader :projectors, :workflows
62
+
63
+ def initialize(result)
64
+ super()
65
+ @result = result
66
+ end
67
+
68
+ def process_event(event)
69
+ Sequent.configuration.event_handlers.each do |handler|
70
+ next unless handler.class.handles_message?(event)
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))
76
+ else
77
+ fail "Unrecognized event_handler #{handler.class} called for event #{event.class}"
78
+ end
79
+ rescue StandardError
80
+ raise PublishEventError.new(handler.class, event)
81
+ end
82
+ end
83
+ end
84
+
85
+ ##
86
+ # Contains the result of a dry run.
87
+ #
88
+ # @see #tree
89
+ # @see #print
90
+ #
91
+ class Result
92
+ EventCalledHandlers = Struct.new(:event, :projectors, :workflows)
93
+
94
+ def initialize
95
+ @command_with_events = {}
96
+ @event_invoked_projectors = []
97
+ @event_invoked_workflows = []
98
+ end
99
+
100
+ def invoked_projector(event_invoked_handler)
101
+ event_invoked_projectors << event_invoked_handler
102
+ end
103
+
104
+ def invoked_workflow(event_invoked_handler)
105
+ event_invoked_workflows << event_invoked_handler
106
+ end
107
+
108
+ def published_command_with_events(command, events)
109
+ command_with_events[command] = events
110
+ end
111
+
112
+ ##
113
+ # Returns the command with events as a tree structure.
114
+ #
115
+ # {
116
+ # command => [
117
+ # EventCalledHandlers,
118
+ # EventCalledHandlers,
119
+ # EventCalledHandlers,
120
+ # ]
121
+ # }
122
+ #
123
+ # The EventCalledHandlers contains an Event with the
124
+ # lists of `Sequent::Projector`s and `Sequent::Workflow`s
125
+ # that were called.
126
+ #
127
+ def tree
128
+ command_with_events.reduce({}) do |memo, (command, events)|
129
+ events_to_handlers = events.map do |event|
130
+ for_current_event = ->(pair) { pair.event == event }
131
+ EventCalledHandlers.new(
132
+ event,
133
+ event_invoked_projectors.select(&for_current_event).map(&:handler),
134
+ event_invoked_workflows.select(&for_current_event).map(&:handler),
135
+ )
136
+ end
137
+ memo[command] = events_to_handlers
138
+ memo
139
+ end
140
+ end
141
+
142
+ ##
143
+ # Prints the output from #tree to the given `io`
144
+ #
145
+ def print(io)
146
+ tree.each_with_index do |(command, event_called_handlerss), index|
147
+ io.puts '+++++++++++++++++++++++++++++++++++' if index == 0
148
+ io.puts "Command: #{command.class} resulted in #{event_called_handlerss.length} events"
149
+ event_called_handlerss.each_with_index do |event_called_handlers, i|
150
+ io.puts '' if i > 0
151
+ io.puts "-- Event #{event_called_handlers.event.class} was handled by:"
152
+ io.puts "-- Projectors: [#{event_called_handlers.projectors.join(', ')}]"
153
+ io.puts "-- Workflows: [#{event_called_handlers.workflows.join(', ')}]"
154
+ end
155
+
156
+ io.puts '+++++++++++++++++++++++++++++++++++'
157
+ end
158
+ end
159
+
160
+ private
161
+
162
+ attr_reader :command_with_events, :event_invoked_projectors, :event_invoked_workflows
163
+ end
164
+
165
+ ##
166
+ # Main method of the DryRun.
167
+ #
168
+ # Caution: Since the Sequent Configuration is changed and is shared between threads this method
169
+ # is not Thread safe.
170
+ #
171
+ # After invocation the sequent configuration is reset to the state it was before
172
+ # invoking this method.
173
+ #
174
+ # @param commands - the commands to dry run
175
+ # @return Result - the Result of the dry run. See Result.
176
+ #
177
+ def self.these_commands(commands)
178
+ current_event_store = Sequent.configuration.event_store
179
+ current_event_publisher = Sequent.configuration.event_publisher
180
+ result = Result.new
181
+
182
+ Sequent.configuration.event_store = EventStoreProxy.new(result)
183
+ Sequent.configuration.event_publisher = RecordingEventPublisher.new(result)
184
+
185
+ Sequent.command_service.execute_commands(*commands)
186
+
187
+ result
188
+ ensure
189
+ Sequent.configuration.event_store = current_event_store
190
+ Sequent.configuration.event_publisher = current_event_publisher
191
+ end
192
+ end
193
+ end
194
+ end
@@ -1,16 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Util
3
5
  module Printer
4
6
  def recursively_print(e)
5
- logger.error "#{e.to_s}\n\n#{e.backtrace.join("\n")}"
7
+ logger.error "#{e}\n\n#{e.backtrace.join("\n")}"
6
8
 
7
- while e.cause do
8
- logger.error "+++++++++++++++ CAUSE +++++++++++++++"
9
- logger.error "#{e.cause.to_s}\n\n#{e.cause.backtrace.join("\n")}"
9
+ while e.cause
10
+ logger.error '+++++++++++++++ CAUSE +++++++++++++++'
11
+ logger.error "#{e.cause}\n\n#{e.cause.backtrace.join("\n")}"
10
12
  e = e.cause
11
13
  end
12
14
  end
13
15
  end
14
16
  end
15
17
  end
16
-
@@ -1,15 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Util
3
- def self.skip_if_already_processing(already_processing_key, &block)
4
- return if Thread.current[already_processing_key]
5
+ ##
6
+ # Returns if the current Thread is already processing work
7
+ # given the +processing_key+ otherwise
8
+ # it yields the given +&block+.
9
+ #
10
+ # Useful in a Queue and Processing strategy
11
+ def self.skip_if_already_processing(processing_key)
12
+ return if Thread.current[processing_key]
5
13
 
6
14
  begin
7
- Thread.current[already_processing_key] = true
15
+ Thread.current[processing_key] = true
8
16
 
9
- block.yield
17
+ yield
10
18
  ensure
11
- Thread.current[already_processing_key] = nil
19
+ Thread.current[processing_key] = nil
12
20
  end
13
21
  end
22
+
23
+ ##
24
+ # Reset the given +processing_key+ for the current Thread.
25
+ #
26
+ # Usefull to make a block protected by +skip_if_already_processing+ reentrant.
27
+ def self.done_processing(processing_key)
28
+ Thread.current[processing_key] = nil
29
+ end
14
30
  end
15
31
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Util
3
5
  module Timer
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'skip_if_already_processing'
2
4
  require_relative 'timer'
3
5
  require_relative 'printer'
6
+ require_relative 'dry_run'
data/lib/sequent.rb CHANGED
@@ -1,5 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'sequent/application_record'
2
4
  require_relative 'sequent/sequent'
3
5
  require_relative 'sequent/core/core'
4
6
  require_relative 'sequent/util/util'
5
7
  require_relative 'sequent/migrations/migrations'
8
+
9
+ require_relative 'notices'