eventsimple 1.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 (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