eventsimple 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +18 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +164 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +320 -0
  9. data/Guardfile +35 -0
  10. data/LICENSE +22 -0
  11. data/README.md +510 -0
  12. data/Rakefile +17 -0
  13. data/app/controllers/eventsimple/application_controller.rb +15 -0
  14. data/app/controllers/eventsimple/entities_controller.rb +59 -0
  15. data/app/controllers/eventsimple/home_controller.rb +5 -0
  16. data/app/controllers/eventsimple/models_controller.rb +10 -0
  17. data/app/views/eventsimple/entities/show.html.erb +109 -0
  18. data/app/views/eventsimple/home/index.html.erb +0 -0
  19. data/app/views/eventsimple/models/show.html.erb +19 -0
  20. data/app/views/eventsimple/shared/_header.html.erb +26 -0
  21. data/app/views/eventsimple/shared/_sidebar.html.erb +19 -0
  22. data/app/views/eventsimple/shared/_style.html.erb +105 -0
  23. data/app/views/layouts/eventsimple/application.html.erb +76 -0
  24. data/catalog-info.yaml +11 -0
  25. data/config/routes.rb +11 -0
  26. data/eventsimple.gemspec +42 -0
  27. data/lib/dry_types.rb +5 -0
  28. data/lib/eventsimple/configuration.rb +36 -0
  29. data/lib/eventsimple/data_type.rb +48 -0
  30. data/lib/eventsimple/dispatcher.rb +17 -0
  31. data/lib/eventsimple/engine.rb +37 -0
  32. data/lib/eventsimple/entity.rb +54 -0
  33. data/lib/eventsimple/event.rb +189 -0
  34. data/lib/eventsimple/event_dispatcher.rb +93 -0
  35. data/lib/eventsimple/generators/install_generator.rb +42 -0
  36. data/lib/eventsimple/generators/outbox/install_generator.rb +31 -0
  37. data/lib/eventsimple/generators/outbox/templates/create_outbox_cursor.erb +13 -0
  38. data/lib/eventsimple/generators/templates/create_events.erb +21 -0
  39. data/lib/eventsimple/generators/templates/event.erb +8 -0
  40. data/lib/eventsimple/invalid_transition.rb +14 -0
  41. data/lib/eventsimple/message.rb +23 -0
  42. data/lib/eventsimple/metadata.rb +11 -0
  43. data/lib/eventsimple/metadata_type.rb +38 -0
  44. data/lib/eventsimple/outbox/consumer.rb +52 -0
  45. data/lib/eventsimple/outbox/models/cursor.rb +25 -0
  46. data/lib/eventsimple/reactor_worker.rb +18 -0
  47. data/lib/eventsimple/support/spec_helpers.rb +47 -0
  48. data/lib/eventsimple/version.rb +5 -0
  49. data/lib/eventsimple.rb +41 -0
  50. data/log/development.log +0 -0
  51. data/sonar-project.properties +4 -0
  52. metadata +304 -0
@@ -0,0 +1,189 @@
1
+ module Eventsimple
2
+ module Event
3
+ require 'globalid'
4
+ include GlobalID::Identification
5
+
6
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
7
+ def drives_events_for(aggregate_klass, aggregate_id:, events_namespace: nil)
8
+ class_attribute :_events_namespace
9
+ self._events_namespace = events_namespace
10
+
11
+ class_attribute :_aggregate_klass
12
+ self._aggregate_klass = aggregate_klass
13
+
14
+ class_attribute :_aggregate_id
15
+ self._aggregate_id = aggregate_id
16
+
17
+ class_attribute :_outbox_mode
18
+ class_attribute :_outbox_concurrency
19
+
20
+ class_attribute :_on_invalid_transition
21
+ self._on_invalid_transition = ->(error) { raise error }
22
+
23
+ self.inheritance_column = :type
24
+ self.store_full_sti_class = false
25
+
26
+ attribute :metadata, MetadataType.new
27
+ attr_writer :skip_dispatcher
28
+ attr_writer :skip_apply_check
29
+
30
+ belongs_to _aggregate_klass.model_name.element.to_sym,
31
+ foreign_key: :aggregate_id,
32
+ primary_key: _aggregate_id,
33
+ class_name: _aggregate_klass.name.to_s,
34
+ inverse_of: :events,
35
+ autosave: false,
36
+ validate: false
37
+
38
+ default_scope { order('created_at ASC') }
39
+
40
+ before_validation :extend_validation
41
+ after_validation :perform_transition_checks
42
+ before_create :apply_and_persist
43
+ after_create :dispatch
44
+
45
+ include InstanceMethods
46
+ extend ClassMethods
47
+ end
48
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
49
+
50
+ module InstanceMethods
51
+ def skip_dispatcher
52
+ @skip_dispatcher || false
53
+ end
54
+
55
+ def skip_apply_check
56
+ @skip_apply_check || false
57
+ end
58
+
59
+ # Apply the event to the aggregate passed in. The default behaviour is a no-op
60
+ def apply(aggregate); end
61
+
62
+ def can_apply?(_aggregate)
63
+ true
64
+ end
65
+
66
+ def apply_timestamps(aggregate)
67
+ aggregate.created_at ||= created_at
68
+ aggregate.updated_at = created_at
69
+ end
70
+
71
+ def perform_transition_checks
72
+ return if skip_apply_check
73
+ return if can_apply?(aggregate)
74
+
75
+ _on_invalid_transition.call(
76
+ Eventsimple::InvalidTransition.new(self.class),
77
+ )
78
+
79
+ raise ActiveRecord::Rollback
80
+ end
81
+
82
+ def extend_validation
83
+ validate_form = self.class.instance_variable_get(:@validate_with)
84
+ self.aggregate = aggregate.extend(validate_form) if validate_form
85
+ end
86
+
87
+ # Apply the transformation to the aggregate and save it.
88
+ def apply_and_persist
89
+ apply(aggregate)
90
+ apply_timestamps(aggregate)
91
+
92
+ # Persist!
93
+ aggregate.save!
94
+
95
+ self.aggregate = aggregate
96
+ end
97
+
98
+ def dispatch
99
+ EventDispatcher.dispatch(self) unless skip_dispatcher
100
+ end
101
+
102
+ def aggregate
103
+ public_send(_aggregate_klass.model_name.element)
104
+ end
105
+
106
+ def aggregate=(aggregate)
107
+ public_send("#{_aggregate_klass.model_name.element}=", aggregate)
108
+ end
109
+ end
110
+
111
+ module ClassMethods
112
+ def validate_with(form_klass)
113
+ @validate_with = form_klass
114
+ end
115
+
116
+ def rescue_invalid_transition(&block)
117
+ self._on_invalid_transition = block || ->(error) {}
118
+ end
119
+
120
+ # We don't store the full namespaced class name in the events table.
121
+ # Events for an entity are expected to be namespaced under _events_namespace.
122
+ def find_sti_class(type_name)
123
+ if _events_namespace.blank?
124
+ super(type_name)
125
+ else
126
+ super("#{_events_namespace}::#{type_name}")
127
+ end
128
+ end
129
+
130
+ # Use a no-op deleted class for events that no longer exist in the codebase
131
+ def sti_class_for(type_name)
132
+ super
133
+ rescue ActiveRecord::SubclassNotFound
134
+ klass_name = "Deleted__#{type_name.demodulize}"
135
+ return const_get(klass_name) if const_defined?(klass_name)
136
+
137
+ # NOTE: this should still update the timestamps for the model to prevent
138
+ # projection drift (since the original projection will
139
+ # have the timestamps applied for the deleted event).
140
+ klass = Class.new(self)
141
+
142
+ const_set(klass_name, klass)
143
+ end
144
+
145
+ # We want to automatically retry writes on concurrency failures. However events with sync
146
+ # reactors may have multiple nested events that are written within the same transaction.
147
+ # We can only catch and retry writes when they the outermost event encapsulating the whole
148
+ # transaction.
149
+ def create(*args, &block)
150
+ with_locks do
151
+ with_retries(args) { super }
152
+ end
153
+ end
154
+
155
+ def create!(*args, &block)
156
+ with_locks do
157
+ with_retries(args) { super }
158
+ end
159
+ end
160
+
161
+ def with_locks(&block)
162
+ if _outbox_mode
163
+ base_class.with_advisory_lock(base_class.name, { transaction: true }, &block)
164
+ else
165
+ yield
166
+ end
167
+ end
168
+
169
+ def with_retries(args, &block) # rubocop:disable Metrics/AbcSize
170
+ entity = args[0][_aggregate_klass.model_name.element.to_sym]
171
+
172
+ # Only implement retries when the event is not already inside a transaction.
173
+ if entity&.persisted? && !existing_transaction_in_progress?
174
+ Retriable.with_context(:optimistic_locking, on_retry: proc { entity.reload }, &block)
175
+ else
176
+ yield
177
+ end
178
+ rescue ActiveRecord::StaleObjectError => e
179
+ raise e unless existing_transaction_in_progress?
180
+
181
+ raise e, "#{e.message} No retries are attempted when already inside a transaction."
182
+ end
183
+
184
+ def existing_transaction_in_progress?
185
+ ActiveRecord::Base.connection.transaction_open?
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2017 Kickstarter, PBC
4
+
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ # Dispatcher implementation.
25
+ module Eventsimple
26
+ class EventDispatcher
27
+ # Dispatches events to matching Reactors once.
28
+ # Called by all events after they are created.
29
+ def self.dispatch(event)
30
+ reactors = rules.for(event)
31
+ reactors.sync.each do |reactor|
32
+ reactor.new(event).call
33
+ event.reload
34
+ end
35
+ reactors.async.each do |reactor|
36
+ ReactorWorker.perform_async(event.to_global_id.to_s, reactor.to_s)
37
+ end
38
+ end
39
+
40
+ def self.rules
41
+ @rules ||= RuleSet.new
42
+ end
43
+
44
+ class RuleSet
45
+ def initialize
46
+ @rules = Hash.new { |h, k| h[k] = ReactorSet.new }
47
+ end
48
+
49
+ # Register events with their sync and async Reactors
50
+ def register(events:, sync:, async:)
51
+ events.each do |event|
52
+ @rules[event].add_sync sync
53
+ @rules[event].add_async async
54
+ end
55
+ end
56
+
57
+ # Return a ReactorSet containing all Reactors matching an Event
58
+ def for(event)
59
+ reactors = ReactorSet.new
60
+
61
+ @rules.each do |event_class, rule|
62
+ # Match event by class including ancestors. e.g. All events match a role for BaseEvent.
63
+ if event.is_a?(event_class)
64
+ reactors.add_sync rule.sync
65
+ reactors.add_async rule.async
66
+ end
67
+ end
68
+
69
+ reactors
70
+ end
71
+ end
72
+
73
+ # Contains sync and async reactors. Used to:
74
+ # * store reactors via Rules#register
75
+ # * return a set of matching reactors with Rules#for
76
+ class ReactorSet
77
+ attr_reader :sync, :async
78
+
79
+ def initialize
80
+ @sync = Set.new
81
+ @async = Set.new
82
+ end
83
+
84
+ def add_sync(reactors)
85
+ @sync += reactors
86
+ end
87
+
88
+ def add_async(reactors)
89
+ @async += reactors
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module Eventsimple
6
+ module Generators
7
+ class EventGenerator < Rails::Generators::Base
8
+ include Rails::Generators::Migration
9
+
10
+ desc "Generate Outbox Table Migration"
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ argument :model_name, type: :string
14
+
15
+ def self.next_migration_number(dirname)
16
+ next_migration_number = current_migration_number(dirname) + 1
17
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
18
+ end
19
+
20
+ def copy_migrations
21
+ migration_template "create_events.erb",
22
+ "db/migrate/create_#{model_name.underscore}_events.rb",
23
+ migration_version: migration_version
24
+
25
+ template "event.erb",
26
+ "app/models/#{model_name.underscore}_event.rb"
27
+
28
+ line = "class #{model_name.camelize} < ApplicationRecord"
29
+ gsub_file "app/models/#{model_name.underscore}.rb", /(#{Regexp.escape(line)})/mi do |match|
30
+ <<~RUBY
31
+ #{match}\n extend Eventsimple::Entity
32
+ event_driven_by #{model_name.camelize}Event, aggregate_id: :id
33
+ RUBY
34
+ end
35
+ end
36
+
37
+ def migration_version
38
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module Eventsimple
6
+ module Generators
7
+ module Outbox
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ desc "Generate Outbox Table Migration"
12
+ source_root File.expand_path("templates", __dir__)
13
+
14
+ def self.next_migration_number(dirname)
15
+ next_migration_number = current_migration_number(dirname) + 1
16
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
17
+ end
18
+
19
+ def copy_migrations
20
+ migration_template "create_outbox_cursor.erb",
21
+ "db/migrate/create_eventsimple_outbox_cursor.rb",
22
+ migration_version: migration_version
23
+ end
24
+
25
+ def migration_version
26
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateEventsimpleOutboxCursor < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :eventsimple_outbox_cursors do |t|
6
+ t.string :event_klass, null: false
7
+ t.integer :group_number, null: false
8
+ t.bigint :cursor, null: false
9
+
10
+ t.index [:event_klass, :group_number], unique: true
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Create<%= model_name.camelize %>Events < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :<%= model_name.underscore %>_events do |t|
6
+ # Change this to string if your aggregates primary key is a string type
7
+ t.bigint :aggregate_id, null: false, index: true
8
+ t.string :idempotency_key, null: true
9
+ t.string :type, null: false
10
+ t.json :data, null: false, default: {}
11
+ t.json :metadata, null: false, default: {}
12
+
13
+ t.timestamps
14
+
15
+ t.index :idempotency_key, unique: true
16
+ end
17
+
18
+ # Enables optimistic locking on the evented table
19
+ add_column :<%= model_name.underscore.pluralize %>, :lock_version, :integer
20
+ end
21
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= model_name.camelize %>Event < ApplicationRecord
4
+ extend Eventsimple::Event
5
+ drives_events_for <%= model_name.camelize %>,
6
+ aggregate_id: :id,
7
+ events_namespace: '<%= model_name.camelize %>Component::Events'
8
+ end
@@ -0,0 +1,14 @@
1
+ module Eventsimple
2
+ class InvalidTransition < StandardError
3
+ attr_reader :klass
4
+
5
+ def initialize(klass = nil)
6
+ @klass = klass
7
+ super
8
+ end
9
+
10
+ def to_s
11
+ "Invalid State Transition for #{klass}"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eventsimple
4
+ class Message < Dry::Struct
5
+ transform_keys(&:to_sym)
6
+
7
+ # dry types will apply default values only on missing keys
8
+ # modify the behaviour so the default is used even when the key is present but nil
9
+ transform_types do |type|
10
+ if type.default?
11
+ type.constructor do |value|
12
+ value.nil? ? Dry::Types::Undefined : value
13
+ end
14
+ else
15
+ type
16
+ end
17
+ end
18
+
19
+ def inspect
20
+ as_json
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Event metadata store information on the event, for example the user who triggered the event.
4
+ module Eventsimple
5
+ class Metadata < Eventsimple::Message
6
+ schema schema.strict
7
+
8
+ attribute? :actor_id, DryTypes::Strict::String
9
+ attribute? :reason, DryTypes::Strict::String
10
+ end
11
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eventsimple
4
+ class MetadataType < ActiveModel::Type::Value
5
+ def type
6
+ :metadata_type
7
+ end
8
+
9
+ def cast_value(value)
10
+ case value
11
+ when String
12
+ decoded = ActiveSupport::JSON.decode(value)
13
+ return decoded if decoded.empty?
14
+
15
+ Eventsimple.configuration.metadata_klass.new(decoded)
16
+ when Hash
17
+ Eventsimple.configuration.metadata_klass.new(value)
18
+ when Eventsimple.configuration.metadata_klass
19
+ value
20
+ end
21
+ end
22
+
23
+ def serialize(value)
24
+ case value
25
+ when Hash, Eventsimple.configuration.metadata_klass
26
+ ActiveSupport::JSON.encode(value)
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ def deserialize(value)
33
+ decoded = ActiveSupport::JSON.decode(value)
34
+
35
+ Eventsimple.configuration.metadata_klass.new(decoded)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eventsimple/outbox/models/cursor'
4
+
5
+ module Eventsimple
6
+ module Outbox
7
+ module Consumer
8
+ def self.extended(klass)
9
+ klass.class_exec do
10
+ class_attribute :_event_klass
11
+ class_attribute :_processor_klass
12
+ class_attribute :stop_consumer, default: false
13
+
14
+ Signal.trap('SIGINT') do
15
+ self.stop_consumer = true
16
+ STDOUT.puts('SIGINT received, stopping consumer')
17
+ end
18
+ end
19
+ end
20
+
21
+ def consumes_event(event_klass, concurrency: 1)
22
+ event_klass._outbox_mode = true
23
+ event_klass._outbox_concurrency = concurrency
24
+
25
+ self._event_klass = event_klass
26
+ end
27
+
28
+ def processor(processor_klass)
29
+ self._processor_klass = processor_klass
30
+ end
31
+
32
+ def start # rubocop:disable Metrics/AbcSize
33
+ cursor = Outbox::Cursor.fetch(_event_klass, 0)
34
+
35
+ until stop_consumer
36
+ _event_klass.unscoped.in_batches(start: cursor + 1, load: true).each do |batch|
37
+ batch.each do |event|
38
+ _processor_klass.new(event).call
39
+
40
+ break if stop_consumer
41
+ end
42
+
43
+ cursor = batch.last.id
44
+ Outbox::Cursor.set(_event_klass, 0, cursor)
45
+ end
46
+
47
+ sleep(1)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eventsimple
4
+ module Outbox
5
+ class Cursor < ApplicationRecord
6
+ self.table_name = 'eventsimple_outbox_cursors'
7
+
8
+ def self.fetch(event_klass, group_number)
9
+ existing = find_by(event_klass: event_klass.to_s, group_number: group_number)
10
+ existing ? existing.cursor : 0
11
+ end
12
+
13
+ def self.set(event_klass, group_number, cursor)
14
+ upsert(
15
+ {
16
+ event_klass: event_klass.to_s,
17
+ group_number: group_number,
18
+ cursor: cursor,
19
+ },
20
+ unique_by: [:event_klass, :group_number],
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eventsimple
4
+ class ReactorWorker
5
+ include Sidekiq::Worker
6
+
7
+ def perform(event_global_id, reactor_class)
8
+ event = Retriable.with_context(:reactor) do
9
+ ApplicationRecord.uncached { GlobalID::Locator.locate event_global_id }
10
+ end
11
+ rescue ActiveRecord::RecordNotFound
12
+ Rails.logger.error("Event #{event_global_id} not found for reactor: #{reactor_class}")
13
+ else
14
+ reactor = reactor_class.constantize
15
+ reactor.new(event).call
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples 'an event which synchronously dispatches' do |dispatcher_klass|
4
+ specify do
5
+ reactors = Eventsimple::EventDispatcher.rules.for(described_class.new)
6
+
7
+ expect(reactors.sync).to include(dispatcher_klass)
8
+ end
9
+ end
10
+
11
+ RSpec.shared_examples 'an event which asynchronously dispatches' do |dispatcher_klass|
12
+ specify do
13
+ reactors = Eventsimple::EventDispatcher.rules.for(described_class.new)
14
+
15
+ expect(reactors.async).to include(dispatcher_klass)
16
+ end
17
+ end
18
+
19
+ RSpec.shared_examples 'an event in invalid state' do
20
+ it 'raises an InvalidTransition error' do
21
+ expect { event.save }.to raise_error(Eventsimple::InvalidTransition).and not_change(
22
+ event.class, :count
23
+ )
24
+ end
25
+ end
26
+
27
+ RSpec.shared_examples 'an event in invalid state that is rescued' do
28
+ context 'when save' do
29
+ it 'does not InvalidTransition error on save' do
30
+ expect { event.save }.not_to raise_error
31
+ end
32
+
33
+ it 'does not write event on save' do
34
+ expect { event.save }.not_to change(event.class, :count)
35
+ end
36
+ end
37
+
38
+ context 'when save!' do
39
+ it 'does not InvalidTransition error on save!' do
40
+ expect { event.save! }.not_to raise_error
41
+ end
42
+
43
+ it 'does not write event on save!' do
44
+ expect { event.save! }.not_to change(event.class, :count)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eventsimple
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "eventsimple/version"
4
+ require "eventsimple/engine"
5
+
6
+ require 'active_model'
7
+ require 'active_support'
8
+ require 'dry-types'
9
+ require 'dry-struct'
10
+ require 'retriable'
11
+ require 'sidekiq'
12
+
13
+ require 'dry_types'
14
+
15
+ require 'eventsimple/configuration'
16
+ require 'eventsimple/message'
17
+ require 'eventsimple/data_type'
18
+ require 'eventsimple/metadata_type'
19
+ require 'eventsimple/metadata'
20
+ require 'eventsimple/dispatcher'
21
+ require 'eventsimple/event_dispatcher'
22
+ require 'eventsimple/reactor_worker'
23
+ require 'eventsimple/invalid_transition'
24
+
25
+ require 'eventsimple/entity'
26
+ require 'eventsimple/event'
27
+
28
+ require 'eventsimple/generators/install_generator'
29
+ require 'eventsimple/generators/outbox/install_generator'
30
+
31
+ module Eventsimple
32
+ class << self
33
+ def configuration
34
+ @configuration ||= Configuration.new
35
+ end
36
+
37
+ def configure
38
+ yield(configuration)
39
+ end
40
+ end
41
+ end
File without changes