diff --git a/aggregate_root/lib/aggregate_root.rb b/aggregate_root/lib/aggregate_root.rb index 87a1bd3bde..c945f8cc14 100644 --- a/aggregate_root/lib/aggregate_root.rb +++ b/aggregate_root/lib/aggregate_root.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true require_relative "aggregate_root/version" -require_relative "aggregate_root/configuration" -require_relative "aggregate_root/transform" require_relative "aggregate_root/default_apply_strategy" require_relative "aggregate_root/repository" require_relative "aggregate_root/instrumented_repository" @@ -11,12 +9,9 @@ module AggregateRoot module OnDSL - ANONYMOUS_CLASS = "# { DefaultApplyStrategy.new }) + host_class.extend Constructor + host_class.include AggregateMethods + end + + define_method :apply_strategy do + DefaultApplyStrategy.new(strict: strict) end end end @@ -90,7 +90,7 @@ def self.included(host_class) def self.with_strategy(strategy) Module.new do def self.included(host_class) - host_class.extend Constructor + host_class.extend Constructor host_class.include AggregateMethods end @@ -101,6 +101,6 @@ def self.included(host_class) end def self.included(host_class) - host_class.include with_default_apply_strategy + host_class.include with_default_strategy end end diff --git a/aggregate_root/lib/aggregate_root/configuration.rb b/aggregate_root/lib/aggregate_root/configuration.rb deleted file mode 100644 index 6935658048..0000000000 --- a/aggregate_root/lib/aggregate_root/configuration.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module AggregateRoot - class << self - attr_accessor :configuration - end - - def self.configure - self.configuration ||= Configuration.new - yield(configuration) - end - - class Configuration - attr_accessor :default_event_store - end -end diff --git a/aggregate_root/lib/aggregate_root/default_apply_strategy.rb b/aggregate_root/lib/aggregate_root/default_apply_strategy.rb index 3046aa2aeb..0249894d05 100644 --- a/aggregate_root/lib/aggregate_root/default_apply_strategy.rb +++ b/aggregate_root/lib/aggregate_root/default_apply_strategy.rb @@ -2,6 +2,7 @@ module AggregateRoot MissingHandler = Class.new(StandardError) + NullHandler = Proc.new {} class DefaultApplyStrategy def initialize(strict: true) @@ -9,32 +10,27 @@ def initialize(strict: true) end def call(aggregate, event) - name = handler_name(aggregate, event) - if aggregate.respond_to?(name, true) - aggregate.method(name).call(event) - else - raise MissingHandler.new("Missing handler method #{name} on aggregate #{aggregate.class}") if strict - end + on_handler(aggregate, event.event_type)[event] end private - def handler_name(aggregate, event) - on_dsl_handler_name(aggregate, event.event_type) || apply_handler_name(event.event_type) - end - - def on_dsl_handler_name(aggregate, event_type) - aggregate.class.on_methods[event_type] if aggregate.class.respond_to?(:on_methods) + def on_handler(aggregate, event_type) + on_method_name = aggregate.class.on_methods.fetch(event_type) + aggregate.method(on_method_name) + rescue KeyError + missing_handler(aggregate, event_type) end - def apply_handler_name(event_type) - "apply_#{Transform.to_snake_case(event_type(event_type))}" - end - - def event_type(event_type) - event_type.split(/::|\./).last + def missing_handler(aggregate, event_type) + if strict + raise MissingHandler.new("Missing handler method on aggregate #{aggregate.class} for #{event_type}") + else + NullHandler + end end attr_reader :strict, :on_methods end + private_constant :DefaultApplyStrategy end diff --git a/aggregate_root/lib/aggregate_root/repository.rb b/aggregate_root/lib/aggregate_root/repository.rb index ad73120d43..09e2ea9575 100644 --- a/aggregate_root/lib/aggregate_root/repository.rb +++ b/aggregate_root/lib/aggregate_root/repository.rb @@ -2,7 +2,7 @@ module AggregateRoot class Repository - def initialize(event_store = default_event_store) + def initialize(event_store) @event_store = event_store end @@ -29,9 +29,5 @@ def with_aggregate(aggregate, stream_name, &block) private attr_reader :event_store - - def default_event_store - AggregateRoot.configuration.default_event_store - end end end diff --git a/aggregate_root/lib/aggregate_root/transform.rb b/aggregate_root/lib/aggregate_root/transform.rb deleted file mode 100644 index 6f996bbbeb..0000000000 --- a/aggregate_root/lib/aggregate_root/transform.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module AggregateRoot - class Transform - def self.to_snake_case(name) - name.gsub(/([A-Z])([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase - end - end -end diff --git a/aggregate_root/spec/aggregate_root_spec.rb b/aggregate_root/spec/aggregate_root_spec.rb index 5c1b8e4b75..71d80d5f56 100644 --- a/aggregate_root/spec/aggregate_root_spec.rb +++ b/aggregate_root/spec/aggregate_root_spec.rb @@ -26,12 +26,14 @@ def expire private - def apply_order_created(_event) - @status = :created + on Orders::Events::OrderCreated do |event| + @status = :created + @created_at = event.valid_at end - def apply_order_expired(_event) - @status = :expired + on Orders::Events::OrderExpired do |event| + @status = :expired + @expired_at = event.valid_at end end end @@ -39,9 +41,8 @@ def apply_order_expired(_event) it "should have ability to apply event on itself" do order = order_klass.new(uuid) order_created = Orders::Events::OrderCreated.new - - expect(order).to receive(:"apply_order_created").with(order_created).and_call_original order.apply(order_created) + expect(order.status).to eq :created expect(order.unpublished_events.to_a).to eq([order_created]) end @@ -51,6 +52,12 @@ def apply_order_expired(_event) expect(order.unpublished_events.to_a).to be_empty end + it "should raise error for missing apply method based on a default apply strategy" do + order = order_klass.new(uuid) + spanish_inquisition = Orders::Events::SpanishInquisition.new + expect { order.apply(spanish_inquisition) }.to raise_error(AggregateRoot::MissingHandler, "Missing handler method on aggregate #{order_klass} for Orders::Events::SpanishInquisition") + end + it "should receive a method call based on a default apply strategy" do order = order_klass.new(uuid) order_created = Orders::Events::OrderCreated.new @@ -59,18 +66,9 @@ def apply_order_expired(_event) expect(order.status).to eq :created end - it "should raise error for missing apply method based on a default apply strategy" do - order = order_klass.new(uuid) - spanish_inquisition = Orders::Events::SpanishInquisition.new - expect { order.apply(spanish_inquisition) }.to raise_error( - AggregateRoot::MissingHandler, - "Missing handler method apply_spanish_inquisition on aggregate #{order_klass}" - ) - end - it "should ignore missing apply method based on a default non-strict apply strategy" do klass = - Class.new { include AggregateRoot.with_strategy(-> { AggregateRoot::DefaultApplyStrategy.new(strict: false) }) } + Class.new { include AggregateRoot.with_default_strategy(strict: false) } order = klass.new spanish_inquisition = Orders::Events::SpanishInquisition.new expect { order.apply(spanish_inquisition) }.to_not raise_error @@ -113,7 +111,7 @@ def custom_expired(_event) end it "ruby 2.7 compatibility" do - klass = Class.new { include AggregateRoot.with_default_apply_strategy } + klass = Class.new { include AggregateRoot.with_default_strategy } # This is just a way to ensure that the AggregateMethods was included on # the klass directly, not that it was an include to the anonymous module. @@ -258,16 +256,6 @@ def custom_expired(_event) order.apply(Orders::Events::OrderExpired.new) expect(order.status).to eq(%i[base_expired inherited_expired]) end - - it "does not support anonymous events" do - expect do - Class.new do - include AggregateRoot - - on(Class.new) { |_ev| } - end - end.to raise_error(ArgumentError, "Anonymous class is missing name") - end end describe "#initialize" do diff --git a/aggregate_root/spec/instrumented_repository_spec.rb b/aggregate_root/spec/instrumented_repository_spec.rb index 344723482f..2a212d89fb 100644 --- a/aggregate_root/spec/instrumented_repository_spec.rb +++ b/aggregate_root/spec/instrumented_repository_spec.rb @@ -27,11 +27,11 @@ def expire private - def apply_order_created(_event) + on Orders::Events::OrderCreated do |_event| @status = :created end - def apply_order_expired(_event) + on Orders::Events::OrderExpired do |_event| @status = :expired end end diff --git a/aggregate_root/spec/repository_spec.rb b/aggregate_root/spec/repository_spec.rb index e388667f2d..690d057f5e 100644 --- a/aggregate_root/spec/repository_spec.rb +++ b/aggregate_root/spec/repository_spec.rb @@ -29,51 +29,16 @@ def expire private - def apply_order_created(_event) + on Orders::Events::OrderCreated do |_event| @status = :created end - def apply_order_expired(_event) + on Orders::Events::OrderExpired do |_event| @status = :expired end end end - def with_default_event_store(store) - previous = AggregateRoot.configuration.default_event_store - AggregateRoot.configure { |config| config.default_event_store = store } - yield - AggregateRoot.configure { |config| config.default_event_store = previous } - end - - describe "#initialize" do - it "should use default client if event_store not provided" do - with_default_event_store(event_store) do - repository = AggregateRoot::Repository.new - - order = repository.load(order_klass.new(uuid), stream_name) - order_created = Orders::Events::OrderCreated.new - order.apply(order_created) - repository.store(order, stream_name) - - expect(event_store.read.stream(stream_name).to_a).to eq [order_created] - end - end - - it "should prefer provided event_store client" do - with_default_event_store(double(:event_store)) do - repository = AggregateRoot::Repository.new(event_store) - - order = repository.load(order_klass.new(uuid), stream_name) - order_created = Orders::Events::OrderCreated.new - order.apply(order_created) - repository.store(order, stream_name) - - expect(event_store.read.stream(stream_name).to_a).to eq [order_created] - end - end - end - describe "#load" do specify do event_store.publish(Orders::Events::OrderCreated.new, stream_name: stream_name) diff --git a/aggregate_root/spec/spec_helper.rb b/aggregate_root/spec/spec_helper.rb index 70dab3ce72..22e40dfcf1 100644 --- a/aggregate_root/spec/spec_helper.rb +++ b/aggregate_root/spec/spec_helper.rb @@ -4,8 +4,6 @@ require "ruby_event_store" require_relative "../../support/helpers/rspec_defaults" -RSpec.configure { |spec| spec.before(:each) { AggregateRoot.configure { |config| config.default_event_store = nil } } } - module Orders module Events OrderCreated = Class.new(RubyEventStore::Event) diff --git a/aggregate_root/spec/transform_spec.rb b/aggregate_root/spec/transform_spec.rb deleted file mode 100644 index 8462b2a14b..0000000000 --- a/aggregate_root/spec/transform_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module AggregateRoot - ::RSpec.describe Transform do - specify { expect(Transform.to_snake_case("OrderSubmitted")).to eq("order_submitted") } - specify { expect(Transform.to_snake_case("OrderSubmittedMultipleTimesReally")).to eq("order_submitted_multiple_times_really") } - specify { expect(Transform.to_snake_case("SHA1ChecksumComputed")).to eq("sha1_checksum_computed") } - specify { expect(Transform.to_snake_case("OKROfPSAInQ1Reached")).to eq("okr_of_psa_in_q1_reached") } - specify { expect(Transform.to_snake_case("EncryptedWithRot13")).to eq("encrypted_with_rot13") } - end -end diff --git a/contrib/dres_rails/Gemfile b/contrib/dres_rails/Gemfile index 9da8a29a5b..a67f69f739 100644 --- a/contrib/dres_rails/Gemfile +++ b/contrib/dres_rails/Gemfile @@ -9,7 +9,6 @@ gem "ruby_event_store", path: "../.." gem "ruby_event_store-browser", path: "../.." gem "rails_event_store", path: "../.." gem "ruby_event_store-active_record", path: "../.." -gem "rails_event_store_active_record", path: "../.." gem "aggregate_root", path: "../.." gem "pg" diff --git a/contrib/dres_rails/Gemfile.lock b/contrib/dres_rails/Gemfile.lock index fd6be18f3d..f6985d56dc 100644 --- a/contrib/dres_rails/Gemfile.lock +++ b/contrib/dres_rails/Gemfile.lock @@ -263,7 +263,6 @@ DEPENDENCIES mutant-rspec (= 0.11.22) pg rails_event_store! - rails_event_store_active_record! rake (>= 10.0) rspec (~> 3.6) rspec-rails diff --git a/contrib/ruby_event_store-outbox/README.md b/contrib/ruby_event_store-outbox/README.md index 49a1488472..60c9c8777e 100644 --- a/contrib/ruby_event_store-outbox/README.md +++ b/contrib/ruby_event_store-outbox/README.md @@ -25,7 +25,7 @@ In your event store configuration, as a dispatcher use `RubyEventStore::Immediat ```ruby RailsEventStore::Client.new( - dispatcher: RailsEventStore::ImmediateAsyncDispatcher.new(scheduler: RubyEventStore::Outbox::SidekiqScheduler.new), + dispatcher: RailsEventStore::ImmediateDispatcher.new(scheduler: RubyEventStore::Outbox::SidekiqScheduler.new), ... ) ``` diff --git a/contrib/ruby_event_store-profiler/README.md b/contrib/ruby_event_store-profiler/README.md index 19d5988b67..d3de56b190 100644 --- a/contrib/ruby_event_store-profiler/README.md +++ b/contrib/ruby_event_store-profiler/README.md @@ -10,7 +10,7 @@ event_store = RubyEventStore::Client.new( repository: RubyEventStore::InstrumentedRepository.new(RubyEventStore::InMemoryRepository.new, instrumenter), mapper: RubyEventStore::Mappers::InstrumentedMapper.new(RubyEventStore::Mappers::Default.new, instrumenter), - dispatcher: RubyEventStore::InstrumentedDispatcher.new(RubyEventStore::Dispatcher.new, instrumenter), + dispatcher: RubyEventStore::InstrumentedDispatcher.new(RubyEventStore::SyncScheduler.new, instrumenter), ) repository = AggregateRoot::InstrumentedRepository.new(AggregateRoot::Repository.new(event_store), instrumenter) diff --git a/contrib/ruby_event_store-profiler/examples/demo b/contrib/ruby_event_store-profiler/examples/demo index bf6cb97f30..57d14d0653 100755 --- a/contrib/ruby_event_store-profiler/examples/demo +++ b/contrib/ruby_event_store-profiler/examples/demo @@ -10,7 +10,7 @@ asn = ActiveSupport::Notifications event_store = RubyEventStore::Client.new( repository: RubyEventStore::InstrumentedRepository.new(RubyEventStore::InMemoryRepository.new, asn), mapper: RubyEventStore::Mappers::InstrumentedMapper.new(RubyEventStore::Mappers::Default.new, asn), - dispatcher: RubyEventStore::InstrumentedDispatcher.new(RubyEventStore::Dispatcher.new, asn) + dispatcher: RubyEventStore::InstrumentedDispatcher.new(RubyEventStore::SyncScheduler.new, asn) ) DummyEvent = Class.new(RubyEventStore::Event) dummy = DummyEvent.new diff --git a/contrib/ruby_event_store-profiler/lib/ruby_event_store/profiler.rb b/contrib/ruby_event_store-profiler/lib/ruby_event_store/profiler.rb index 6fdd3314c1..e5efbae937 100644 --- a/contrib/ruby_event_store-profiler/lib/ruby_event_store/profiler.rb +++ b/contrib/ruby_event_store-profiler/lib/ruby_event_store/profiler.rb @@ -1,6 +1,6 @@ module RubyEventStore class Profiler - METRICS = [/rails_event_store/, /aggregate_root/, "total"].freeze + METRICS = [/ruby_event_store/, /aggregate_root/, "total"].freeze private_constant :METRICS def initialize(instrumenter) diff --git a/contrib/ruby_event_store-profiler/spec/profiler_spec.rb b/contrib/ruby_event_store-profiler/spec/profiler_spec.rb index bdaeb6efa0..26a328a27e 100644 --- a/contrib/ruby_event_store-profiler/spec/profiler_spec.rb +++ b/contrib/ruby_event_store-profiler/spec/profiler_spec.rb @@ -7,7 +7,7 @@ module RubyEventStore Client.new( repository: InstrumentedRepository.new(InMemoryRepository.new, instrumenter), mapper: Mappers::InstrumentedMapper.new(Mappers::Default.new, instrumenter), - dispatcher: InstrumentedDispatcher.new(Dispatcher.new, instrumenter) + dispatcher: InstrumentedDispatcher.new(SyncScheduler.new, instrumenter) ) end @@ -37,7 +37,7 @@ def now expect { Profiler.new(instrumenter).measure(&operation) }.to output(<<~EOS).to_stdout metric ms % ───────────────────────────────── - serialize 1000.00 16.67 + event_to_record 1000.00 16.67 append_to_stream 1000.00 16.67 total 6000.00 100.00 @@ -56,7 +56,7 @@ def now $stdout = STDOUT end - expect(return_value).to eq({ "total" => 6000, "serialize" => 1000.0, "append_to_stream" => 1000.0 }) + expect(return_value).to eq({ "total" => 6000, "event_to_record" => 1000.0, "append_to_stream" => 1000.0 }) end end end diff --git a/contrib/ruby_event_store-protobuf/Gemfile b/contrib/ruby_event_store-protobuf/Gemfile index 8b839c76b5..12e0b0919a 100644 --- a/contrib/ruby_event_store-protobuf/Gemfile +++ b/contrib/ruby_event_store-protobuf/Gemfile @@ -7,7 +7,6 @@ gem "ruby_event_store", path: "../.." gem "aggregate_root", path: "../.." gem "rails_event_store", path: "../.." gem "ruby_event_store-active_record", path: "../.." -gem "rails_event_store_active_record", path: "../.." gem "ruby_event_store-browser", path: "../.." gem "protobuf_nested_struct" diff --git a/contrib/ruby_event_store-protobuf/Gemfile.lock b/contrib/ruby_event_store-protobuf/Gemfile.lock index a552e2cb8c..c0dd3c703d 100644 --- a/contrib/ruby_event_store-protobuf/Gemfile.lock +++ b/contrib/ruby_event_store-protobuf/Gemfile.lock @@ -239,7 +239,6 @@ DEPENDENCIES protobuf_nested_struct rails (~> 7.0.7) rails_event_store! - rails_event_store_active_record! rake (>= 10.0) rspec (~> 3.6) ruby_event_store! diff --git a/contrib/ruby_event_store-protobuf/lib/ruby_event_store/protobuf/mappers/protobuf.rb b/contrib/ruby_event_store-protobuf/lib/ruby_event_store/protobuf/mappers/protobuf.rb index 7b1ba95705..2c6083b9f0 100644 --- a/contrib/ruby_event_store-protobuf/lib/ruby_event_store/protobuf/mappers/protobuf.rb +++ b/contrib/ruby_event_store-protobuf/lib/ruby_event_store/protobuf/mappers/protobuf.rb @@ -10,11 +10,10 @@ def event_type module Mappers class Protobuf < RubyEventStore::Mappers::PipelineMapper - def initialize(events_class_remapping: {}) + def initialize super( RubyEventStore::Mappers::Pipeline.new( Transformation::ProtobufEncoder.new, - RubyEventStore::Mappers::Transformation::EventClassRemapper.new(events_class_remapping), Transformation::ProtobufNestedStructMetadata.new, to_domain_event: Transformation::ProtoEvent.new ) diff --git a/contrib/ruby_event_store-protobuf/spec/aggregate_root_spec.rb b/contrib/ruby_event_store-protobuf/spec/aggregate_root_spec.rb index 20d08499e8..c75f51f866 100644 --- a/contrib/ruby_event_store-protobuf/spec/aggregate_root_spec.rb +++ b/contrib/ruby_event_store-protobuf/spec/aggregate_root_spec.rb @@ -34,7 +34,7 @@ def initialize(uuid) private - def apply_order_created(*) + on "res_testing.OrderCreated" do |_event| @status = :created end @@ -78,7 +78,7 @@ def apply_order_created(*) expect { order.apply(spanish_inquisition) }.to raise_error( AggregateRoot::MissingHandler, - "Missing handler method apply_spanish_inquisition on aggregate ResTesting::Order" + "Missing handler method on aggregate ResTesting::Order for res_testing.SpanishInquisition" ) end end diff --git a/contrib/ruby_event_store-protobuf/spec/mappers/protobuf_spec.rb b/contrib/ruby_event_store-protobuf/spec/mappers/protobuf_spec.rb index a1499ee8d6..9d1f53876d 100644 --- a/contrib/ruby_event_store-protobuf/spec/mappers/protobuf_spec.rb +++ b/contrib/ruby_event_store-protobuf/spec/mappers/protobuf_spec.rb @@ -187,7 +187,7 @@ module Mappers expect(record).to be_a(Record) expect(record.event_id).to eq(event_id) expect(record.data).not_to be_empty - expect(record.metadata).not_to be_empty + expect(record.metadata).to match(/\n.+\x00/) expect(record.event_type).to eq("res_testing.OrderCreated") expect(record.timestamp).to eq(time) expect(record.valid_at).to eq(time) @@ -203,29 +203,6 @@ module Mappers expect(event.metadata[:timestamp]).to eq(time) expect(event.metadata[:valid_at]).to eq(time) end - - specify "#record_to_event is using events class remapping" do - subject = - RubyEventStore::Protobuf::Mappers::Protobuf.new( - events_class_remapping: { - "res_testing.OrderCreatedBeforeRefactor" => "res_testing.OrderCreated" - } - ) - record = - Record.new( - event_id: "f90b8848-e478-47fe-9b4a-9f2a1d53622b", - data: "", - metadata: "", - event_type: "res_testing.OrderCreatedBeforeRefactor", - timestamp: time, - valid_at: time - ) - event = subject.record_to_event(record) - expect(event.data.class).to eq(ResTesting::OrderCreated) - expect(event.event_type).to eq("res_testing.OrderCreated") - expect(event.metadata[:timestamp]).to eq(time) - expect(event.metadata[:valid_at]).to eq(time) - end end end end diff --git a/contrib/ruby_event_store-protobuf/spec/protobuf_integration_spec.rb b/contrib/ruby_event_store-protobuf/spec/protobuf_integration_spec.rb index e06ab8c743..0bb04c2068 100644 --- a/contrib/ruby_event_store-protobuf/spec/protobuf_integration_spec.rb +++ b/contrib/ruby_event_store-protobuf/spec/protobuf_integration_spec.rb @@ -27,8 +27,8 @@ module RubyEventStore mapper: Protobuf::Mappers::Protobuf.new, dispatcher: ComposedDispatcher.new( - ImmediateAsyncDispatcher.new(scheduler: RailsEventStore::ActiveJobScheduler.new(serializer: NULL)), - Dispatcher.new + ImmediateDispatcher.new(scheduler: RailsEventStore::ActiveJobScheduler.new(serializer: NULL)), + SyncScheduler.new ) ) client.subscribe(->(ev) { @ev = ev }, to: [ResTesting::OrderCreated.descriptor.name]) diff --git a/contrib/ruby_event_store-sidekiq_scheduler/README.md b/contrib/ruby_event_store-sidekiq_scheduler/README.md index 63350ff595..4812224a4d 100644 --- a/contrib/ruby_event_store-sidekiq_scheduler/README.md +++ b/contrib/ruby_event_store-sidekiq_scheduler/README.md @@ -16,8 +16,8 @@ Declare the scheduler in your Ruby Event Store configuration. We recommend to us ```ruby event_store = RailsEventStore::Client.new( - dispatcher: RailsEventStore::AfterCommitAsyncDispatcher.new(scheduler: RubyEventStore::SidekiqScheduler.new(serializer: RubyEventStore::Serializers::YAML), -) + dispatcher: RailsEventStore::AfterCommitDispatcher.new(scheduler: RubyEventStore::SidekiqScheduler.new(serializer: RubyEventStore::Serializers::YAML), + ) ``` Read more about [using asynchronous handlers](https://railseventstore.org/docs/v2/subscribe/#async-handlers) diff --git a/contrib/ruby_event_store-sidekiq_scheduler/spec/async_handler_helpers_spec.rb b/contrib/ruby_event_store-sidekiq_scheduler/spec/async_handler_helpers_spec.rb index 325bd41842..8d722fb58b 100644 --- a/contrib/ruby_event_store-sidekiq_scheduler/spec/async_handler_helpers_spec.rb +++ b/contrib/ruby_event_store-sidekiq_scheduler/spec/async_handler_helpers_spec.rb @@ -13,7 +13,7 @@ def perform(event) let(:event_store) do RubyEventStore::Client.new( dispatcher: - ImmediateAsyncDispatcher.new( + ImmediateDispatcher.new( scheduler: SidekiqScheduler.new(serializer: serializer) ) ) @@ -26,7 +26,7 @@ def perform(event) SidekiqHandlerWithHelper.prepend( RailsEventStore::AsyncHandler.with( - event_store: event_store, + event_store_locator: ->{ event_store }, serializer: serializer ) ) diff --git a/contrib/ruby_event_store-transformations/lib/ruby_event_store/transformations.rb b/contrib/ruby_event_store-transformations/lib/ruby_event_store/transformations.rb index 4ebb692552..a335959bde 100644 --- a/contrib/ruby_event_store-transformations/lib/ruby_event_store/transformations.rb +++ b/contrib/ruby_event_store-transformations/lib/ruby_event_store/transformations.rb @@ -3,6 +3,7 @@ require_relative "transformations/version" require_relative "transformations/with_indifferent_access" require_relative "transformations/identity_map" +require_relative "transformations/event_class_remapper" module RubyEventStore module Transformations diff --git a/contrib/ruby_event_store-transformations/lib/ruby_event_store/transformations/event_class_remapper.rb b/contrib/ruby_event_store-transformations/lib/ruby_event_store/transformations/event_class_remapper.rb new file mode 100644 index 0000000000..63082d2451 --- /dev/null +++ b/contrib/ruby_event_store-transformations/lib/ruby_event_store/transformations/event_class_remapper.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module RubyEventStore + module Transformations + class EventClassRemapper + def initialize(class_map) + @class_map = class_map + end + + def dump(record) + record + end + + def load(record) + Record.new( + event_id: record.event_id, + event_type: class_map[record.event_type] || record.event_type, + data: record.data, + metadata: record.metadata, + timestamp: record.timestamp, + valid_at: record.valid_at + ) + end + + private + + attr_reader :class_map + end + end +end diff --git a/contrib/ruby_event_store-transformations/spec/event_class_remapper_spec.rb b/contrib/ruby_event_store-transformations/spec/event_class_remapper_spec.rb new file mode 100644 index 0000000000..24cfbea921 --- /dev/null +++ b/contrib/ruby_event_store-transformations/spec/event_class_remapper_spec.rb @@ -0,0 +1,39 @@ +require "spec_helper" + +module RubyEventStore + module Transformations + ::RSpec.describe EventClassRemapper do + let(:time) { Time.now.utc } + let(:uuid) { SecureRandom.uuid } + + def record(event_type: "TestEvent") + Record.new( + event_id: uuid, + metadata: { + some: "meta" + }, + data: { + some: "value" + }, + event_type: event_type, + timestamp: time, + valid_at: time + ) + end + + let(:changeable_record) { record(event_type: "EventNameBeforeRefactor") } + let(:changed_record) { record(event_type: "SomethingHappened") } + let(:class_map) { { "EventNameBeforeRefactor" => "SomethingHappened" } } + + specify "#dump" do + expect(EventClassRemapper.new(class_map).dump(record)).to eq(record) + expect(EventClassRemapper.new(class_map).dump(record)).to eq(record) + end + + specify "#load" do + expect(EventClassRemapper.new(class_map).load(record)).to eq(record) + expect(EventClassRemapper.new(class_map).load(changeable_record)).to eq(changed_record) + end + end + end +end diff --git a/rails_event_store/Gemfile b/rails_event_store/Gemfile index 9f0b8ca73c..eba9e23c9d 100644 --- a/rails_event_store/Gemfile +++ b/rails_event_store/Gemfile @@ -6,7 +6,6 @@ eval_gemfile "../support/bundler/Gemfile.shared" gem "ruby_event_store", path: ".." gem "ruby_event_store-browser", path: ".." gem "ruby_event_store-active_record", path: ".." -gem "rails_event_store_active_record", path: ".." gem "aggregate_root", path: ".." gem "sidekiq" diff --git a/rails_event_store/Gemfile.lock b/rails_event_store/Gemfile.lock index 821a820718..77fdfd67be 100644 --- a/rails_event_store/Gemfile.lock +++ b/rails_event_store/Gemfile.lock @@ -249,7 +249,6 @@ DEPENDENCIES rack-test rails (~> 7.0.7) rails_event_store! - rails_event_store_active_record! rake (>= 10.0) rspec (~> 3.6) ruby_event_store! diff --git a/rails_event_store/Gemfile.rails_6_0 b/rails_event_store/Gemfile.rails_6_0 index daf8a685da..6a2c9a6c14 100644 --- a/rails_event_store/Gemfile.rails_6_0 +++ b/rails_event_store/Gemfile.rails_6_0 @@ -6,7 +6,6 @@ eval_gemfile "../support/bundler/Gemfile.shared" gem "ruby_event_store", path: ".." gem "ruby_event_store-browser", path: ".." gem "ruby_event_store-active_record", path: ".." -gem "rails_event_store_active_record", path: ".." gem "aggregate_root", path: ".." gem "sidekiq" diff --git a/rails_event_store/Gemfile.rails_6_0.lock b/rails_event_store/Gemfile.rails_6_0.lock index b8a5c28898..cba23b4c07 100644 --- a/rails_event_store/Gemfile.rails_6_0.lock +++ b/rails_event_store/Gemfile.rails_6_0.lock @@ -247,7 +247,6 @@ DEPENDENCIES rack-test rails (~> 6.0.6) rails_event_store! - rails_event_store_active_record! rake (>= 10.0) rspec (~> 3.6) ruby_event_store! diff --git a/rails_event_store/Gemfile.rails_6_1 b/rails_event_store/Gemfile.rails_6_1 index f6b07c2806..942f9f2f2e 100644 --- a/rails_event_store/Gemfile.rails_6_1 +++ b/rails_event_store/Gemfile.rails_6_1 @@ -6,7 +6,6 @@ eval_gemfile "../support/bundler/Gemfile.shared" gem "ruby_event_store", path: ".." gem "ruby_event_store-browser", path: ".." gem "ruby_event_store-active_record", path: ".." -gem "rails_event_store_active_record", path: ".." gem "aggregate_root", path: ".." gem "sidekiq" diff --git a/rails_event_store/Gemfile.rails_6_1.lock b/rails_event_store/Gemfile.rails_6_1.lock index 9f497f1b5d..a8aa665154 100644 --- a/rails_event_store/Gemfile.rails_6_1.lock +++ b/rails_event_store/Gemfile.rails_6_1.lock @@ -250,7 +250,6 @@ DEPENDENCIES rack-test rails (~> 6.1.7) rails_event_store! - rails_event_store_active_record! rake (>= 10.0) rspec (~> 3.6) ruby_event_store! diff --git a/rails_event_store/lib/rails_event_store.rb b/rails_event_store/lib/rails_event_store.rb index 8ebeb44b37..3f9af5d76a 100644 --- a/rails_event_store/lib/rails_event_store.rb +++ b/rails_event_store/lib/rails_event_store.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true -require "rails_event_store_active_record" +require "ruby_event_store/active_record" require_relative "rails_event_store/all" diff --git a/rails_event_store/lib/rails_event_store/after_commit_async_dispatcher.rb b/rails_event_store/lib/rails_event_store/after_commit_dispatcher.rb similarity index 96% rename from rails_event_store/lib/rails_event_store/after_commit_async_dispatcher.rb rename to rails_event_store/lib/rails_event_store/after_commit_dispatcher.rb index 38633a2bf1..837c8b1cbf 100644 --- a/rails_event_store/lib/rails_event_store/after_commit_async_dispatcher.rb +++ b/rails_event_store/lib/rails_event_store/after_commit_dispatcher.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module RailsEventStore - class AfterCommitAsyncDispatcher + class AfterCommitDispatcher def initialize(scheduler:) @scheduler = scheduler end diff --git a/rails_event_store/lib/rails_event_store/all.rb b/rails_event_store/lib/rails_event_store/all.rb index a34890350d..be80be2653 100644 --- a/rails_event_store/lib/rails_event_store/all.rb +++ b/rails_event_store/lib/rails_event_store/all.rb @@ -3,7 +3,7 @@ require "ruby_event_store" require_relative "async_handler_helpers" require_relative "link_by_metadata" -require_relative "after_commit_async_dispatcher" +require_relative "after_commit_dispatcher" require_relative "active_job_scheduler" require_relative 'active_job_id_only_scheduler' require_relative "client" @@ -11,23 +11,3 @@ require_relative "version" require_relative "railtie" require_relative "browser" - -module RailsEventStore - Event = RubyEventStore::Event - InMemoryRepository = RubyEventStore::InMemoryRepository - Subscriptions = RubyEventStore::Subscriptions - Projection = RubyEventStore::Projection - WrongExpectedEventVersion = RubyEventStore::WrongExpectedEventVersion - InvalidExpectedVersion = RubyEventStore::InvalidExpectedVersion - IncorrectStreamData = RubyEventStore::IncorrectStreamData - EventNotFound = RubyEventStore::EventNotFound - SubscriberNotExist = RubyEventStore::SubscriberNotExist - InvalidHandler = RubyEventStore::InvalidHandler - InvalidPageStart = RubyEventStore::InvalidPageStart - InvalidPageStop = RubyEventStore::InvalidPageStop - InvalidPageSize = RubyEventStore::InvalidPageSize - CorrelatedCommands = RubyEventStore::CorrelatedCommands - GLOBAL_STREAM = RubyEventStore::GLOBAL_STREAM - PAGE_SIZE = RubyEventStore::PAGE_SIZE - ImmediateAsyncDispatcher = RubyEventStore::ImmediateAsyncDispatcher -end diff --git a/rails_event_store/lib/rails_event_store/async_handler_helpers.rb b/rails_event_store/lib/rails_event_store/async_handler_helpers.rb index a0b1393b4d..b64d2cba71 100644 --- a/rails_event_store/lib/rails_event_store/async_handler_helpers.rb +++ b/rails_event_store/lib/rails_event_store/async_handler_helpers.rb @@ -7,14 +7,12 @@ def self.with_defaults end def self.with( - event_store: Rails.configuration.event_store, - event_store_locator: nil, - serializer: RubyEventStore::Serializers::YAML + serializer: RubyEventStore::Serializers::YAML, + event_store_locator: -> { Rails.configuration.event_store } ) Module.new do define_method :perform do |payload| - event_store = event_store_locator.call if event_store_locator - super(event_store.deserialize(serializer: serializer, **payload.transform_keys(&:to_sym))) + super(event_store_locator.call.deserialize(serializer: serializer, **payload.transform_keys(&:to_sym))) end end end diff --git a/rails_event_store/lib/rails_event_store/client.rb b/rails_event_store/lib/rails_event_store/client.rb index ed2f2734b6..efcc338612 100644 --- a/rails_event_store/lib/rails_event_store/client.rb +++ b/rails_event_store/lib/rails_event_store/client.rb @@ -9,10 +9,10 @@ def initialize( repository: RubyEventStore::ActiveRecord::EventRepository.new(serializer: RubyEventStore::Serializers::YAML), subscriptions: RubyEventStore::Subscriptions.new, dispatcher: RubyEventStore::ComposedDispatcher.new( - RailsEventStore::AfterCommitAsyncDispatcher.new( + RailsEventStore::AfterCommitDispatcher.new( scheduler: ActiveJobScheduler.new(serializer: RubyEventStore::Serializers::YAML) ), - RubyEventStore::Dispatcher.new + RubyEventStore::SyncScheduler.new ), clock: default_clock, correlation_id_generator: default_correlation_id_generator, diff --git a/rails_event_store/lib/rails_event_store/json_client.rb b/rails_event_store/lib/rails_event_store/json_client.rb index d1f5108403..fd520de743 100644 --- a/rails_event_store/lib/rails_event_store/json_client.rb +++ b/rails_event_store/lib/rails_event_store/json_client.rb @@ -30,10 +30,10 @@ def initialize( repository: RubyEventStore::ActiveRecord::EventRepository.new(serializer: RubyEventStore::NULL), subscriptions: RubyEventStore::Subscriptions.new, dispatcher: RubyEventStore::ComposedDispatcher.new( - RailsEventStore::AfterCommitAsyncDispatcher.new( + RailsEventStore::AfterCommitDispatcher.new( scheduler: ActiveJobScheduler.new(serializer: RubyEventStore::Serializers::YAML) ), - RubyEventStore::Dispatcher.new + RubyEventStore::SyncScheduler.new ), clock: default_clock, correlation_id_generator: default_correlation_id_generator, diff --git a/rails_event_store/spec/active_job_id_only_scheduler_spec.rb b/rails_event_store/spec/active_job_id_only_scheduler_spec.rb index 9b605aa218..c4d6791513 100644 --- a/rails_event_store/spec/active_job_id_only_scheduler_spec.rb +++ b/rails_event_store/spec/active_job_id_only_scheduler_spec.rb @@ -24,7 +24,7 @@ module RailsEventStore it_behaves_like :scheduler, ActiveJobIdOnlyScheduler.new - let(:event) { TimeEnrichment.with(Event.new(event_id: "83c3187f-84f6-4da7-8206-73af5aca7cc8"), timestamp: Time.utc(2019, 9, 30)) } + let(:event) { TimeEnrichment.with(RubyEventStore::Event.new(event_id: "83c3187f-84f6-4da7-8206-73af5aca7cc8"), timestamp: Time.utc(2019, 9, 30)) } let(:record) { RubyEventStore::Mappers::Default.new.event_to_record(event) } describe "#verify" do diff --git a/rails_event_store/spec/active_job_scheduler_spec.rb b/rails_event_store/spec/active_job_scheduler_spec.rb index 9040cd5c9f..0a7444d0ec 100644 --- a/rails_event_store/spec/active_job_scheduler_spec.rb +++ b/rails_event_store/spec/active_job_scheduler_spec.rb @@ -23,9 +23,7 @@ module RailsEventStore it_behaves_like :scheduler, ActiveJobScheduler.new(serializer: RubyEventStore::Serializers::YAML) it_behaves_like :scheduler, ActiveJobScheduler.new(serializer: RubyEventStore::NULL) - let(:event) do - TimeEnrichment.with(Event.new(event_id: "83c3187f-84f6-4da7-8206-73af5aca7cc8"), timestamp: Time.utc(2019, 9, 30)) - end + let(:event) { TimeEnrichment.with(RubyEventStore::Event.new(event_id: "83c3187f-84f6-4da7-8206-73af5aca7cc8"), timestamp: Time.utc(2019, 9, 30)) } let(:record) { RubyEventStore::Mappers::Default.new.event_to_record(event) } describe "#verify" do diff --git a/rails_event_store/spec/after_commit_async_dispatcher_spec.rb b/rails_event_store/spec/after_commit_dispatcher_spec.rb similarity index 95% rename from rails_event_store/spec/after_commit_async_dispatcher_spec.rb rename to rails_event_store/spec/after_commit_dispatcher_spec.rb index 59e799e574..4e9c6ca3ac 100644 --- a/rails_event_store/spec/after_commit_async_dispatcher_spec.rb +++ b/rails_event_store/spec/after_commit_dispatcher_spec.rb @@ -2,7 +2,7 @@ require "ruby_event_store/spec/dispatcher_lint" module RailsEventStore - ::RSpec.describe AfterCommitAsyncDispatcher do + ::RSpec.describe AfterCommitDispatcher do DummyError = Class.new(StandardError) class DummyRecord < ActiveRecord::Base @@ -10,13 +10,13 @@ class DummyRecord < ActiveRecord::Base after_commit -> { raise DummyError } end - it_behaves_like :dispatcher, AfterCommitAsyncDispatcher.new(scheduler: ActiveJobScheduler.new(serializer: RubyEventStore::Serializers::YAML)) + it_behaves_like :dispatcher, AfterCommitDispatcher.new(scheduler: ActiveJobScheduler.new(serializer: RubyEventStore::Serializers::YAML)) let(:event) { TimeEnrichment.with(RubyEventStore::Event.new(event_id: "83c3187f-84f6-4da7-8206-73af5aca7cc8")) } let(:record) { RubyEventStore::Mappers::Default.new.event_to_record(event) } let(:serialized_record) { record.serialize(RubyEventStore::Serializers::YAML).to_h.transform_keys(&:to_s) } - let(:dispatcher) { AfterCommitAsyncDispatcher.new(scheduler: ActiveJobScheduler.new(serializer: RubyEventStore::Serializers::YAML)) } + let(:dispatcher) { AfterCommitDispatcher.new(scheduler: ActiveJobScheduler.new(serializer: RubyEventStore::Serializers::YAML)) } before(:each) { MyActiveJobAsyncHandler.reset } diff --git a/rails_event_store/spec/async_handler_helpers_spec.rb b/rails_event_store/spec/async_handler_helpers_spec.rb index 86d79f4c75..c532d3adc4 100644 --- a/rails_event_store/spec/async_handler_helpers_spec.rb +++ b/rails_event_store/spec/async_handler_helpers_spec.rb @@ -71,7 +71,7 @@ def perform(event) let(:another_event_store) { RailsEventStore::Client.new } let(:json_event_store) do RailsEventStore::Client.new( - dispatcher: RubyEventStore::ImmediateAsyncDispatcher.new(scheduler: ActiveJobScheduler.new(serializer: JSON)) + dispatcher: RubyEventStore::ImmediateDispatcher.new(scheduler: ActiveJobScheduler.new(serializer: JSON)) ) end let(:application) { instance_double(Rails::Application) } @@ -95,7 +95,7 @@ def perform(event) end specify "with specified event store" do - HandlerWithAnotherEventStore.prepend RailsEventStore::AsyncHandler.with(event_store: another_event_store) + HandlerWithAnotherEventStore.prepend RailsEventStore::AsyncHandler.with(event_store_locator: -> { another_event_store} ) event_store.subscribe_to_all_events(HandlerWithAnotherEventStore) event_store.publish(ev = RubyEventStore::Event.new) expect($queue.pop).to eq(ev) @@ -103,7 +103,6 @@ def perform(event) specify "with specified event store locator" do HandlerWithEventStoreLocator.prepend RailsEventStore::AsyncHandler.with( - event_store: nil, event_store_locator: -> { another_event_store } ) another_event_store.subscribe_to_all_events(HandlerWithEventStoreLocator) @@ -113,7 +112,7 @@ def perform(event) specify "with specified serializer" do HandlerWithSpecifiedSerializer.prepend RailsEventStore::AsyncHandler.with( - event_store: json_event_store, + event_store_locator: -> { json_event_store }, serializer: JSON ) json_event_store.subscribe_to_all_events(HandlerWithSpecifiedSerializer) @@ -184,7 +183,7 @@ def perform(event) specify "ActiveJob with AsyncHandlerJobIdOnly prepended with event store locator" do HandlerE = Class.new(IdOnlyHandler) - HandlerE.prepend AsyncHandlerJobIdOnly.with(event_store: nil, event_store_locator: -> { event_store }) + HandlerE.prepend AsyncHandlerJobIdOnly.with(event_store_locator: -> { event_store }) event_store.subscribe_to_all_events(HandlerE) event_store.publish(ev = RubyEventStore::Event.new) diff --git a/rails_event_store/spec/client_spec.rb b/rails_event_store/spec/client_spec.rb index ba258e8b4d..127280fa83 100644 --- a/rails_event_store/spec/client_spec.rb +++ b/rails_event_store/spec/client_spec.rb @@ -37,7 +37,7 @@ module RailsEventStore client = Client.new(repository: RubyEventStore::InMemoryRepository.new) received_notifications = 0 - ActiveSupport::Notifications.subscribe("append_to_stream.repository.rails_event_store") do + ActiveSupport::Notifications.subscribe("append_to_stream.repository.ruby_event_store") do received_notifications += 1 end @@ -48,9 +48,8 @@ module RailsEventStore specify "wraps mapper into instrumentation" do client = Client.new(repository: RubyEventStore::InMemoryRepository.new, mapper: RubyEventStore::Mappers::Default.new) - received_notifications = 0 - ActiveSupport::Notifications.subscribe("serialize.mapper.rails_event_store") { received_notifications += 1 } + ActiveSupport::Notifications.subscribe("event_to_record.mapper.ruby_event_store") { received_notifications += 1 } client.publish(TestEvent.new) diff --git a/rails_event_store/spec/dummy_6_0/Gemfile b/rails_event_store/spec/dummy_6_0/Gemfile index ec14e91477..58380cf3c5 100644 --- a/rails_event_store/spec/dummy_6_0/Gemfile +++ b/rails_event_store/spec/dummy_6_0/Gemfile @@ -4,7 +4,6 @@ gem "rails_event_store", path: "../../" gem "ruby_event_store", path: "../../../ruby_event_store" gem "ruby_event_store-browser", path: "../../../ruby_event_store-browser" gem "ruby_event_store-active_record", path: "../../../ruby_event_store-active_record" -gem "rails_event_store_active_record", path: "../../../ruby_event_store-active_record" gem "aggregate_root", path: "../../../aggregate_root" gem "rails", "~> 6.0.6" \ No newline at end of file diff --git a/rails_event_store/spec/dummy_6_0/Gemfile.lock b/rails_event_store/spec/dummy_6_0/Gemfile.lock index faad4fdfc0..0c8a382bbe 100644 --- a/rails_event_store/spec/dummy_6_0/Gemfile.lock +++ b/rails_event_store/spec/dummy_6_0/Gemfile.lock @@ -201,7 +201,6 @@ DEPENDENCIES aggregate_root! rails (~> 6.0.6) rails_event_store! - rails_event_store_active_record! ruby_event_store! ruby_event_store-active_record! ruby_event_store-browser! diff --git a/rails_event_store/spec/dummy_6_1/Gemfile b/rails_event_store/spec/dummy_6_1/Gemfile index 02d5c185dd..e5c0b71215 100644 --- a/rails_event_store/spec/dummy_6_1/Gemfile +++ b/rails_event_store/spec/dummy_6_1/Gemfile @@ -4,7 +4,6 @@ gem "rails_event_store", path: "../../" gem "ruby_event_store", path: "../../../ruby_event_store" gem "ruby_event_store-browser", path: "../../../ruby_event_store-browser" gem "ruby_event_store-active_record", path: "../../../ruby_event_store-active_record" -gem "rails_event_store_active_record", path: "../../../ruby_event_store-active_record" gem "aggregate_root", path: "../../../aggregate_root" gem "rails", "~> 6.1.7" \ No newline at end of file diff --git a/rails_event_store/spec/dummy_6_1/Gemfile.lock b/rails_event_store/spec/dummy_6_1/Gemfile.lock index cb46a1cadc..8ca723b343 100644 --- a/rails_event_store/spec/dummy_6_1/Gemfile.lock +++ b/rails_event_store/spec/dummy_6_1/Gemfile.lock @@ -204,7 +204,6 @@ DEPENDENCIES aggregate_root! rails (~> 6.1.7) rails_event_store! - rails_event_store_active_record! ruby_event_store! ruby_event_store-active_record! ruby_event_store-browser! diff --git a/rails_event_store/spec/dummy_7_0/Gemfile b/rails_event_store/spec/dummy_7_0/Gemfile index 7a5794a0a8..d7071a63eb 100644 --- a/rails_event_store/spec/dummy_7_0/Gemfile +++ b/rails_event_store/spec/dummy_7_0/Gemfile @@ -4,7 +4,6 @@ gem "rails_event_store", path: "../../" gem "ruby_event_store", path: "../../../ruby_event_store" gem "ruby_event_store-browser", path: "../../../ruby_event_store-browser" gem "ruby_event_store-active_record", path: "../../../ruby_event_store-active_record" -gem "rails_event_store_active_record", path: "../../../ruby_event_store-active_record" gem "aggregate_root", path: "../../../aggregate_root" gem "rails", "~> 7.0.7" \ No newline at end of file diff --git a/rails_event_store/spec/dummy_7_0/Gemfile.lock b/rails_event_store/spec/dummy_7_0/Gemfile.lock index 32adc128db..9b0f1791b6 100644 --- a/rails_event_store/spec/dummy_7_0/Gemfile.lock +++ b/rails_event_store/spec/dummy_7_0/Gemfile.lock @@ -198,7 +198,6 @@ DEPENDENCIES aggregate_root! rails (~> 7.0.7) rails_event_store! - rails_event_store_active_record! ruby_event_store! ruby_event_store-active_record! ruby_event_store-browser! diff --git a/ruby_event_store-active_record/spec/migration_generator_spec.rb b/rails_event_store/spec/migration_generator_spec.rb similarity index 100% rename from ruby_event_store-active_record/spec/migration_generator_spec.rb rename to rails_event_store/spec/migration_generator_spec.rb diff --git a/railseventstore.org/config.rb b/railseventstore.org/config.rb index 2bcc9fa79f..979a87e15c 100644 --- a/railseventstore.org/config.rb +++ b/railseventstore.org/config.rb @@ -39,4 +39,5 @@ def precompiled_template(locals) page "/" page "/docs/v1/*", locals: { version: "v1" }, layout: "documentation" page "/docs/v2/*", locals: { version: "v2" }, layout: "documentation" +page "/docs/v3/*", locals: { version: "v3" }, layout: "documentation" page "*", locals: { version: "v2" } diff --git a/railseventstore.org/source/docs/v3/serialization.html.md.erb b/railseventstore.org/source/docs/v3/serialization.html.md.erb new file mode 100644 index 0000000000..d90e8edab3 --- /dev/null +++ b/railseventstore.org/source/docs/v3/serialization.html.md.erb @@ -0,0 +1,134 @@ +--- +title: Event serialization formats +--- + +By default RailsEventStore will use `YAML` as a +serialization format. The reason is that `YAML` is available out of box +and can serialize and deserialize data types which are not easily +handled in other formats. + +However, if you don't like `YAML` or you have different needs you can +choose to use different serializers or even replace mappers entirely. + +## Configuring a different serializer + +You can pass a different `serializer` as a dependency when [instantiating +the client](/docs/v2/install). + +Here is an example on how to configure RailsEventStore to serialize +events' `data` and `metadata` using `Marshal`. + +```ruby +# config/environments/*.rb + +Rails.application.configure do + config.to_prepare do + Rails.configuration.event_store = RailsEventStore::Client.new( + repository: RailsEventStoreActiveRecord::EventRepository.new(serializer: Marshal) + ) + end +end +``` + +The provided `serializer` must respond to `load` and `dump`. + +Serialization is needed not only when writing to and reading from storage, but also when scheduling events for background processing by async handlers: + +```ruby +Rails.configuration.event_store = RailsEventStore::Client.new( + dispatcher: RubyEventStore::ComposedDispatcher.new( + RailsEventStore::AfterCommitAsyncDispatcher.new(scheduler: ActiveJobScheduler.new(serializer: Marshal)), + RubyEventStore::Dispatcher.new + ) + ) +``` + +```ruby +class SomeHandler < ActiveJob::Base + include RailsEventStore::AsyncHandler.with(serializer: Marshal) + + def perform(event) + # ... + end +end +``` + +## Configuring for Postgres JSON/B data type + +In Postgres database, you can store your events data and metadata in json or jsonb format. + +To generate migration containing event table schemas run + +```console +$ rails generate rails_event_store_active_record:migration --data-type=jsonb +``` + +Next, in your `RailsEventStore::Client` initialization, set repository serialization to ` RailsEventStoreActiveRecord::EventRepository.new(serializer: JSON)` + +```ruby +# config/environments/*.rb + +Rails.application.configure do + config.to_prepare do + Rails.configuration.event_store = RailsEventStore::Client.new( + repository: RubyEventStore::ActiveRecord::EventRepository.new(serializer: JSON) + ) + end +end +``` + +Using the `JSON` serializer (or any compatible, e.g. `ActiveSupport::JSON`) will serialize the event data and metadata. +It will do otherwise when reading. +ActiveRecord serialization will be skipped. +Database itself expect data to be json already. + + diff --git a/railseventstore.org/source/partials/_documentation_nav_v3.erb b/railseventstore.org/source/partials/_documentation_nav_v3.erb new file mode 100644 index 0000000000..734110247a --- /dev/null +++ b/railseventstore.org/source/partials/_documentation_nav_v3.erb @@ -0,0 +1,21 @@ +

+ Getting started +

+ +

+ Core concepts +

+ +

+ Advanced topics +

+ +

+ Common usage patterns +

+ \ No newline at end of file diff --git a/ruby_event_store-active_record/Gemfile b/ruby_event_store-active_record/Gemfile index fb17e08762..3acd431ac0 100644 --- a/ruby_event_store-active_record/Gemfile +++ b/ruby_event_store-active_record/Gemfile @@ -7,3 +7,4 @@ eval_gemfile "../support/bundler/Gemfile.database" gem "ruby_event_store", path: ".." gem "childprocess" + diff --git a/ruby_event_store-active_record/lib/rails_event_store_active_record.rb b/ruby_event_store-active_record/lib/rails_event_store_active_record.rb index e3e9e3a5fd..58f1539e70 100644 --- a/ruby_event_store-active_record/lib/rails_event_store_active_record.rb +++ b/ruby_event_store-active_record/lib/rails_event_store_active_record.rb @@ -1,3 +1,13 @@ require "ruby_event_store/active_record" +warn <<~EOW + The 'rails_event_store_active_record' gem has been renamed. + + Please change your Gemfile or gemspec + to reflect its new name: + + 'ruby_event_store-active_record' + +EOW + RailsEventStoreActiveRecord = RubyEventStore::ActiveRecord diff --git a/ruby_event_store-active_record/lib/ruby_event_store/active_record/event.rb b/ruby_event_store-active_record/lib/ruby_event_store/active_record/event.rb index 0536d7c21b..56eab9e396 100644 --- a/ruby_event_store-active_record/lib/ruby_event_store/active_record/event.rb +++ b/ruby_event_store-active_record/lib/ruby_event_store/active_record/event.rb @@ -7,6 +7,15 @@ module ActiveRecord class Event < ::ActiveRecord::Base self.primary_key = :id self.table_name = "event_store_events" + + if Gem::Version.new(::ActiveRecord::VERSION::STRING) >= Gem::Version.new("6.1.0") + skip_json_serialization = ->(initial_column_type) do + %i[json jsonb].include?(initial_column_type.type) ? ActiveModel::Type::Value.new : initial_column_type + end + + attribute :data, skip_json_serialization + attribute :metadata, skip_json_serialization + end end private_constant :Event diff --git a/ruby_event_store-active_record/rails_event_store_active_record.gemspec b/ruby_event_store-active_record/rails_event_store_active_record.gemspec index 5515f604cb..7740823c2d 100644 --- a/ruby_event_store-active_record/rails_event_store_active_record.gemspec +++ b/ruby_event_store-active_record/rails_event_store_active_record.gemspec @@ -30,4 +30,13 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 2.7" spec.add_dependency "ruby_event_store-active_record", RubyEventStore::ActiveRecord::VERSION + spec.post_install_message = <<~EOW + The 'rails_event_store_active_record' gem has been renamed. + + Please change your Gemfile or gemspec + to reflect its new name: + + 'ruby_event_store-active-record' + + EOW end diff --git a/ruby_event_store-active_record/spec/pg_linearized_event_repository_spec.rb b/ruby_event_store-active_record/spec/pg_linearized_event_repository_spec.rb index 9ab59ab1f5..2a6db64079 100644 --- a/ruby_event_store-active_record/spec/pg_linearized_event_repository_spec.rb +++ b/ruby_event_store-active_record/spec/pg_linearized_event_repository_spec.rb @@ -6,7 +6,7 @@ module RubyEventStore module ActiveRecord ::RSpec.describe PgLinearizedEventRepository do helper = SpecHelper.new - mk_repository = -> { PgLinearizedEventRepository.new(serializer: Serializers::YAML) } + mk_repository = -> { PgLinearizedEventRepository.new(serializer: helper.serializer) } it_behaves_like :event_repository, mk_repository, helper diff --git a/ruby_event_store-active_record/spec/skip_ar_serialization_spec.rb b/ruby_event_store-active_record/spec/skip_ar_serialization_spec.rb new file mode 100644 index 0000000000..c8e6ee067a --- /dev/null +++ b/ruby_event_store-active_record/spec/skip_ar_serialization_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require "spec_helper" +require "ruby_event_store" +require "ruby_event_store/spec/event_repository_lint" + +module RubyEventStore + module ActiveRecord + ::RSpec.describe "Skip ActiveRecord serialization of data and metadata for json(b) columns" do + helper = SpecHelper.new + mk_repository = -> { EventRepository.new(serializer: JSON) } + + let(:repository) { mk_repository.call } + let(:specification) do + RubyEventStore::Specification.new( + RubyEventStore::SpecificationReader.new(repository, RubyEventStore::Mappers::Default.new) + ) + end + + around(:each) { |example| helper.run_lifecycle { example.run } } + + specify do + repository.append_to_stream( + [RubyEventStore::SRecord.new(data: { "foo" => "bar" })], + RubyEventStore::Stream.new("stream"), + RubyEventStore::ExpectedVersion.auto + ) + + record = repository.read(specification.result).first + expect(record.data).to eq({ "foo" => "bar" }) + expect( + ::ActiveRecord::Base + .connection + .execute("SELECT data ->> 'foo' as foo FROM event_store_events ORDER BY created_at DESC") + .first[ + "foo" + ] + ).to eq("bar") + end if ENV["DATABASE_URL"].include?("postgres") && %w[json jsonb].include?(ENV["DATA_TYPE"]) + + specify do + repository.append_to_stream( + [RubyEventStore::SRecord.new(data: { "foo" => "bar" })], + RubyEventStore::Stream.new("stream"), + RubyEventStore::ExpectedVersion.auto + ) + + record = repository.read(specification.result).first + expect(record.data).to eq({ "foo" => "bar" }) + end + end + end +end diff --git a/ruby_event_store-active_record/spec/spec_helper.rb b/ruby_event_store-active_record/spec/spec_helper.rb index c62ae47498..97d2047b01 100644 --- a/ruby_event_store-active_record/spec/spec_helper.rb +++ b/ruby_event_store-active_record/spec/spec_helper.rb @@ -16,7 +16,12 @@ class SpecHelper include SchemaHelper def serializer - Serializers::YAML + case ENV["DATA_TYPE"] + when /json/ + JSON + else + Serializers::YAML + end end def run_lifecycle diff --git a/ruby_event_store-rspec/spec/spec_helper.rb b/ruby_event_store-rspec/spec/spec_helper.rb index 8dd5035a34..258c5014d2 100644 --- a/ruby_event_store-rspec/spec/spec_helper.rb +++ b/ruby_event_store-rspec/spec/spec_helper.rb @@ -9,7 +9,7 @@ BazEvent = Class.new(RubyEventStore::Event) class TestAggregate - include AggregateRoot.with_strategy(-> { AggregateRoot::DefaultApplyStrategy.new(strict: false) }) + include AggregateRoot.with_default_strategy(strict: false) def foo apply(FooEvent.new) diff --git a/ruby_event_store/lib/ruby_event_store.rb b/ruby_event_store/lib/ruby_event_store.rb index 50be5761f7..7b0a5f3fa0 100644 --- a/ruby_event_store/lib/ruby_event_store.rb +++ b/ruby_event_store/lib/ruby_event_store.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "ruby_event_store/dispatcher" +require_relative "ruby_event_store/sync_scheduler" require_relative "ruby_event_store/subscriptions" require_relative "ruby_event_store/broker" require_relative "ruby_event_store/in_memory_repository" @@ -23,7 +23,6 @@ require_relative "ruby_event_store/mappers/in_memory_encryption_key_repository" require_relative "ruby_event_store/mappers/transformation/domain_event" require_relative "ruby_event_store/mappers/transformation/encryption" -require_relative "ruby_event_store/mappers/transformation/event_class_remapper" require_relative "ruby_event_store/mappers/transformation/upcast" require_relative "ruby_event_store/mappers/transformation/stringify_metadata_keys" require_relative "ruby_event_store/mappers/transformation/symbolize_metadata_keys" @@ -34,16 +33,14 @@ require_relative "ruby_event_store/mappers/forgotten_data" require_relative "ruby_event_store/mappers/encryption_mapper" require_relative "ruby_event_store/mappers/instrumented_mapper" -require_relative "ruby_event_store/mappers/json_mapper" -require_relative "ruby_event_store/mappers/null_mapper" require_relative "ruby_event_store/serializers/yaml" require_relative "ruby_event_store/batch_enumerator" require_relative "ruby_event_store/correlated_commands" require_relative "ruby_event_store/link_by_metadata" -require_relative "ruby_event_store/immediate_async_dispatcher" +require_relative "ruby_event_store/immediate_dispatcher" require_relative "ruby_event_store/composed_dispatcher" require_relative "ruby_event_store/version" require_relative "ruby_event_store/instrumented_repository" require_relative "ruby_event_store/instrumented_dispatcher" require_relative "ruby_event_store/instrumented_subscriptions" -require_relative "ruby_event_store/event_type_resolver" \ No newline at end of file +require_relative "ruby_event_store/event_type_resolver" diff --git a/ruby_event_store/lib/ruby_event_store/client.rb b/ruby_event_store/lib/ruby_event_store/client.rb index 0ed4217195..e0444099e3 100644 --- a/ruby_event_store/lib/ruby_event_store/client.rb +++ b/ruby_event_store/lib/ruby_event_store/client.rb @@ -8,7 +8,7 @@ def initialize( repository: InMemoryRepository.new, mapper: Mappers::Default.new, subscriptions: Subscriptions.new, - dispatcher: Dispatcher.new, + dispatcher: SyncScheduler.new, clock: default_clock, correlation_id_generator: default_correlation_id_generator, event_type_resolver: EventTypeResolver.new @@ -30,6 +30,8 @@ def initialize( # @param expected_version [:any, :auto, :none, Integer] controls optimistic locking strategy. {http://railseventstore.org/docs/expected_version/ Read more} # @return [self] def publish(events, stream_name: GLOBAL_STREAM, expected_version: :any) + assert_nil_events(events) + enriched_events = enrich_events_metadata(events) records = transform(enriched_events) append_records_to_stream(records, stream_name: stream_name, expected_version: expected_version) @@ -46,6 +48,8 @@ def publish(events, stream_name: GLOBAL_STREAM, expected_version: :any) # @param (see #publish) # @return [self] def append(events, stream_name: GLOBAL_STREAM, expected_version: :any) + assert_nil_events(events) + append_records_to_stream( transform(enrich_events_metadata(events)), stream_name: stream_name, @@ -62,6 +66,8 @@ def append(events, stream_name: GLOBAL_STREAM, expected_version: :any) # @param expected_version (see #publish) # @return [self] def link(event_ids, stream_name:, expected_version: :any) + assert_nil_events(event_ids) + repository.link_to_stream(Array(event_ids), Stream.new(stream_name), ExpectedVersion.new(expected_version)) self end @@ -338,6 +344,12 @@ def inspect private + def assert_nil_events(events) + raise ArgumentError, "Event cannot be `nil`" if events.nil? + events = Array(events) + raise ArgumentError, "Event cannot be `nil`" if events.any?(&:nil?) + end + def transform(events) events.map { |ev| mapper.event_to_record(ev) } end diff --git a/ruby_event_store/lib/ruby_event_store/dispatcher.rb b/ruby_event_store/lib/ruby_event_store/dispatcher.rb deleted file mode 100644 index a1d3fda68c..0000000000 --- a/ruby_event_store/lib/ruby_event_store/dispatcher.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -module RubyEventStore - class Dispatcher - def call(subscriber, event, _) - subscriber = subscriber.new if Class === subscriber - subscriber.call(event) - end - - def verify(subscriber) - begin - subscriber_instance = Class === subscriber ? subscriber.new : subscriber - rescue ArgumentError - false - else - subscriber_instance.respond_to?(:call) - end - end - end -end diff --git a/ruby_event_store/lib/ruby_event_store/immediate_async_dispatcher.rb b/ruby_event_store/lib/ruby_event_store/immediate_dispatcher.rb similarity index 89% rename from ruby_event_store/lib/ruby_event_store/immediate_async_dispatcher.rb rename to ruby_event_store/lib/ruby_event_store/immediate_dispatcher.rb index e9f908ceb3..5f9b2fa855 100644 --- a/ruby_event_store/lib/ruby_event_store/immediate_async_dispatcher.rb +++ b/ruby_event_store/lib/ruby_event_store/immediate_dispatcher.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module RubyEventStore - class ImmediateAsyncDispatcher + class ImmediateDispatcher def initialize(scheduler:) @scheduler = scheduler end diff --git a/ruby_event_store/lib/ruby_event_store/in_memory_repository.rb b/ruby_event_store/lib/ruby_event_store/in_memory_repository.rb index 3549d23373..bbd9f14d9c 100644 --- a/ruby_event_store/lib/ruby_event_store/in_memory_repository.rb +++ b/ruby_event_store/lib/ruby_event_store/in_memory_repository.rb @@ -23,7 +23,7 @@ def initialize(event_id, position) attr_reader :event_id, :position end - def initialize(serializer: NULL, ensure_supported_any_usage: false) + def initialize(serializer: NULL, ensure_supported_any_usage: true) @serializer = serializer @streams = Hash.new { |h, k| h[k] = Array.new } @mutex = Mutex.new diff --git a/ruby_event_store/lib/ruby_event_store/instrumented_dispatcher.rb b/ruby_event_store/lib/ruby_event_store/instrumented_dispatcher.rb index c1171c7b9d..a9f3c84f7a 100644 --- a/ruby_event_store/lib/ruby_event_store/instrumented_dispatcher.rb +++ b/ruby_event_store/lib/ruby_event_store/instrumented_dispatcher.rb @@ -8,7 +8,7 @@ def initialize(dispatcher, instrumentation) end def call(subscriber, event, record) - instrumentation.instrument("call.dispatcher.rails_event_store", event: event, subscriber: subscriber) do + instrumentation.instrument("call.dispatcher.ruby_event_store", event: event, subscriber: subscriber) do dispatcher.call(subscriber, event, record) end end diff --git a/ruby_event_store/lib/ruby_event_store/instrumented_repository.rb b/ruby_event_store/lib/ruby_event_store/instrumented_repository.rb index a70311818c..b2133a1794 100644 --- a/ruby_event_store/lib/ruby_event_store/instrumented_repository.rb +++ b/ruby_event_store/lib/ruby_event_store/instrumented_repository.rb @@ -8,43 +8,43 @@ def initialize(repository, instrumentation) end def append_to_stream(records, stream, expected_version) - instrumentation.instrument("append_to_stream.repository.rails_event_store", events: records, stream: stream) do + instrumentation.instrument("append_to_stream.repository.ruby_event_store", records: records, stream: stream) do repository.append_to_stream(records, stream, expected_version) end end def link_to_stream(event_ids, stream, expected_version) - instrumentation.instrument("link_to_stream.repository.rails_event_store", event_ids: event_ids, stream: stream) do + instrumentation.instrument("link_to_stream.repository.ruby_event_store", event_ids: event_ids, stream: stream) do repository.link_to_stream(event_ids, stream, expected_version) end end def delete_stream(stream) - instrumentation.instrument("delete_stream.repository.rails_event_store", stream: stream) do + instrumentation.instrument("delete_stream.repository.ruby_event_store", stream: stream) do repository.delete_stream(stream) end end def read(specification) - instrumentation.instrument("read.repository.rails_event_store", specification: specification) do + instrumentation.instrument("read.repository.ruby_event_store", specification: specification) do repository.read(specification) end end def count(specification) - instrumentation.instrument("count.repository.rails_event_store", specification: specification) do + instrumentation.instrument("count.repository.ruby_event_store", specification: specification) do repository.count(specification) end end - def update_messages(messages) - instrumentation.instrument("update_messages.repository.rails_event_store", messages: messages) do - repository.update_messages(messages) + def update_messages(records) + instrumentation.instrument("update_messages.repository.ruby_event_store", records: records) do + repository.update_messages(records) end end def streams_of(event_id) - instrumentation.instrument("streams_of.repository.rails_event_store", event_id: event_id) do + instrumentation.instrument("streams_of.repository.ruby_event_store", event_id: event_id) do repository.streams_of(event_id) end end diff --git a/ruby_event_store/lib/ruby_event_store/mappers/default.rb b/ruby_event_store/lib/ruby_event_store/mappers/default.rb index 259fd8b555..70ffec09bb 100644 --- a/ruby_event_store/lib/ruby_event_store/mappers/default.rb +++ b/ruby_event_store/lib/ruby_event_store/mappers/default.rb @@ -3,13 +3,10 @@ module RubyEventStore module Mappers class Default < PipelineMapper - def initialize(events_class_remapping: {}) - super( - Pipeline.new( - Transformation::EventClassRemapper.new(events_class_remapping), - Transformation::SymbolizeMetadataKeys.new - ) - ) + def initialize + super(Pipeline.new( + Transformation::SymbolizeMetadataKeys.new, + )) end end end diff --git a/ruby_event_store/lib/ruby_event_store/mappers/instrumented_mapper.rb b/ruby_event_store/lib/ruby_event_store/mappers/instrumented_mapper.rb index 74afe07369..3e5f42dc8f 100644 --- a/ruby_event_store/lib/ruby_event_store/mappers/instrumented_mapper.rb +++ b/ruby_event_store/lib/ruby_event_store/mappers/instrumented_mapper.rb @@ -9,13 +9,13 @@ def initialize(mapper, instrumentation) end def event_to_record(event) - instrumentation.instrument("serialize.mapper.rails_event_store", domain_event: event) do + instrumentation.instrument("event_to_record.mapper.ruby_event_store", event: event) do mapper.event_to_record(event) end end def record_to_event(record) - instrumentation.instrument("deserialize.mapper.rails_event_store", record: record) do + instrumentation.instrument("record_to_event.mapper.ruby_event_store", record: record) do mapper.record_to_event(record) end end diff --git a/ruby_event_store/lib/ruby_event_store/mappers/json_mapper.rb b/ruby_event_store/lib/ruby_event_store/mappers/json_mapper.rb deleted file mode 100644 index 93f64773d0..0000000000 --- a/ruby_event_store/lib/ruby_event_store/mappers/json_mapper.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module RubyEventStore - module Mappers - class JSONMapper < Default - def initialize(events_class_remapping: {}) - warn <<~EOW - Please replace RubyEventStore::Mappers::JSONMapper with RubyEventStore::Mappers::Default - - They're now identical and the former will be removed in next major release. - EOW - super - end - end - end -end diff --git a/ruby_event_store/lib/ruby_event_store/mappers/null_mapper.rb b/ruby_event_store/lib/ruby_event_store/mappers/null_mapper.rb deleted file mode 100644 index 21f34b30df..0000000000 --- a/ruby_event_store/lib/ruby_event_store/mappers/null_mapper.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module RubyEventStore - module Mappers - class NullMapper < PipelineMapper - def initialize - super(Pipeline.new) - end - end - end -end diff --git a/ruby_event_store/lib/ruby_event_store/mappers/transformation/event_class_remapper.rb b/ruby_event_store/lib/ruby_event_store/mappers/transformation/event_class_remapper.rb deleted file mode 100644 index 7f4084864d..0000000000 --- a/ruby_event_store/lib/ruby_event_store/mappers/transformation/event_class_remapper.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -module RubyEventStore - module Mappers - module Transformation - class EventClassRemapper - def initialize(class_map) - @class_map = class_map - end - - def dump(record) - record - end - - def load(record) - Record.new( - event_id: record.event_id, - event_type: class_map[record.event_type] || record.event_type, - data: record.data, - metadata: record.metadata, - timestamp: record.timestamp, - valid_at: record.valid_at - ) - end - - private - - attr_reader :class_map - end - end - end -end diff --git a/ruby_event_store/lib/ruby_event_store/projection.rb b/ruby_event_store/lib/ruby_event_store/projection.rb index d06a1a62a0..755872b157 100644 --- a/ruby_event_store/lib/ruby_event_store/projection.rb +++ b/ruby_event_store/lib/ruby_event_store/projection.rb @@ -4,92 +4,45 @@ module RubyEventStore class Projection private_class_method :new - def self.from_stream(stream_or_streams) - streams = Array(stream_or_streams) - raise(ArgumentError, "At least one stream must be given") if streams.empty? - new(streams: streams) + def initialize(initial_state, event_type_resolver) + @handlers = {} + @event_type_resolver = event_type_resolver + @init = -> { initial_state } end - def self.from_all_streams - new + def self.init(initial_state = nil, event_type_resolver: EventTypeResolver.new) + new(initial_state, event_type_resolver) end - def initialize(streams: []) - @streams = streams - @handlers = {} - @init = -> { {} } - end + def on(*event_klasses, &block) + raise(ArgumentError, 'No handler block given') unless block_given? - attr_reader :streams, :handlers + event_klasses.each do |event_klass| + name = @event_type_resolver.call(event_klass) - def init(handler) - @init = handler + @handlers[name] = ->(state, event) { block.call(state, event) } + end self end - def when(events, handler) - Array(events).each { |event| handlers[event.to_s] = handler } + def call(scope) + return initial_state if handled_events.empty? - self + scope.of_types(handled_events).reduce(initial_state, &method(:transition)) end + private + def initial_state @init.call end - def current_state - @current_state ||= initial_state - end - - def call(event) - handlers.fetch(event.event_type).(current_state, event) - end - def handled_events - handlers.keys - end - - def run(event_store, start: nil, count: PAGE_SIZE) - return initial_state if handled_events.empty? - streams.any? ? reduce_from_streams(event_store, start, count) : reduce_from_all_streams(event_store, start, count) - end - - private - - def valid_starting_point?(start) - return true unless start - streams.any? ? (start.instance_of?(Array) && start.size === streams.size) : start.instance_of?(String) - end - - def reduce_from_streams(event_store, start, count) - raise ArgumentError.new("Start must be an array with event ids") unless valid_starting_point?(start) - streams - .zip(start_events(start)) - .reduce(initial_state) do |state, (stream_name, start_event_id)| - read_scope(event_store, stream_name, count, start_event_id).reduce(state, &method(:transition)) - end - end - - def reduce_from_all_streams(event_store, start, count) - raise ArgumentError.new("Start must be valid event id") unless valid_starting_point?(start) - read_scope(event_store, nil, count, start).reduce(initial_state, &method(:transition)) - end - - def read_scope(event_store, stream, count, start) - scope = event_store.read.in_batches(count) - scope = scope.of_type(handled_events) - scope = scope.stream(stream) if stream - scope = scope.from(start) if start - scope - end - - def start_events(start) - start ? start : Array.new + @handlers.keys end def transition(state, event) - handlers.fetch(event.event_type).call(state, event) - state + @handlers.fetch(event.event_type).call(state, event) end end end diff --git a/ruby_event_store/lib/ruby_event_store/spec/broker_lint.rb b/ruby_event_store/lib/ruby_event_store/spec/broker_lint.rb index 07f6538c21..0c338d68e9 100644 --- a/ruby_event_store/lib/ruby_event_store/spec/broker_lint.rb +++ b/ruby_event_store/lib/ruby_event_store/spec/broker_lint.rb @@ -3,7 +3,7 @@ let(:record) { instance_double(::RubyEventStore::Record) } let(:handler) { HandlerClass.new } let(:subscriptions) { ::RubyEventStore::Subscriptions.new } - let(:dispatcher) { ::RubyEventStore::Dispatcher.new } + let(:dispatcher) { ::RubyEventStore::SyncScheduler.new } let(:broker) { broker_klass.new(subscriptions: subscriptions, dispatcher: dispatcher) } specify "no dispatch when no subscriptions" do @@ -48,7 +48,7 @@ allow(dispatcher).to receive(:verify).and_return(false) expect { broker.add_subscription(HandlerClass, []) }.to raise_error( RubyEventStore::InvalidHandler, - /Handler HandlerClass is invalid for dispatcher .*Dispatcher/ + /Handler HandlerClass is invalid for dispatcher .*SyncScheduler/ ) expect { broker.add_global_subscription(HandlerClass) }.to raise_error( RubyEventStore::InvalidHandler, diff --git a/ruby_event_store/lib/ruby_event_store/sync_scheduler.rb b/ruby_event_store/lib/ruby_event_store/sync_scheduler.rb new file mode 100644 index 0000000000..d9cf2b75b6 --- /dev/null +++ b/ruby_event_store/lib/ruby_event_store/sync_scheduler.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module RubyEventStore + class SyncScheduler + def call(subscriber, event, _) + subscriber.call(event) + end + + def verify(subscriber) + subscriber.respond_to?(:call) + end + end +end diff --git a/ruby_event_store/spec/client_spec.rb b/ruby_event_store/spec/client_spec.rb index ee3b4fe39f..b05592901b 100644 --- a/ruby_event_store/spec/client_spec.rb +++ b/ruby_event_store/spec/client_spec.rb @@ -13,10 +13,57 @@ module RubyEventStore expect(client.publish(TestEvent.new)).to eq(client) end + specify "publish with no events, fail if nil" do + client.publish([], stream_name: stream) + + expect { + client.publish(nil, stream_name: stream) + }.to raise_error(ArgumentError, "Event cannot be `nil`") + + expect { + client.publish([nil], stream_name: stream) + }.to raise_error(ArgumentError, "Event cannot be `nil`") + + expect(client.read.stream(stream).to_a).to be_empty + end + specify "append returns client when success" do expect(client.append(TestEvent.new, stream_name: stream)).to eq(client) end + specify "append with no events, fail if nil" do + client.append([], stream_name: stream) + + expect { + client.append(nil, stream_name: stream) + }.to raise_error(ArgumentError, "Event cannot be `nil`") + + expect { + client.append([nil], stream_name: stream) + }.to raise_error(ArgumentError, "Event cannot be `nil`") + + expect(client.read.stream(stream).to_a).to be_empty + end + + specify "link returns self when success" do + client.append(event = TestEvent.new) + expect(client.link(event.event_id, stream_name: stream)).to eq(client) + end + + specify "link with no events, fail if nil" do + client.link([], stream_name: stream) + + expect { + client.link(nil, stream_name: stream) + }.to raise_error(ArgumentError, "Event cannot be `nil`") + + expect { + client.link([nil], stream_name: stream) + }.to raise_error(ArgumentError, "Event cannot be `nil`") + + expect(client.read.stream(stream).to_a).to be_empty + end + specify "append to default stream when not specified" do expect(client.append(test_event = TestEvent.new)).to eq(client) expect(client.read.limit(100).to_a).to eq([test_event]) @@ -192,7 +239,7 @@ module RubyEventStore expect(published[2].metadata[:valid_at]).to be_a Time end - specify "event's metadata takes precedence over with_metadata" do + specify "event's metadata takes precedence over with_metadata" do client.with_metadata(request_ip: "127.0.0.1") do client.publish(@event = TestEvent.new(metadata: { request_ip: "1.2.3.4" })) end @@ -879,7 +926,7 @@ def self.event_type Client.new( repository: InMemoryRepository.new(serializer: serializer), dispatcher: - ImmediateAsyncDispatcher.new( + ImmediateDispatcher.new( scheduler: ScheduledWithSerialization.new(serializer: serializer) ) ) @@ -901,7 +948,7 @@ def self.event_type Client.new( repository: InMemoryRepository.new(serializer: serializer_1), dispatcher: - ImmediateAsyncDispatcher.new( + ImmediateDispatcher.new( scheduler: ScheduledWithSerialization.new(serializer: serializer_2) ) ) diff --git a/ruby_event_store/spec/immediate_async_dispatcher_spec.rb b/ruby_event_store/spec/immediate_dispatcher_spec.rb similarity index 73% rename from ruby_event_store/spec/immediate_async_dispatcher_spec.rb rename to ruby_event_store/spec/immediate_dispatcher_spec.rb index c9fc5e93fc..9cfef0563d 100644 --- a/ruby_event_store/spec/immediate_async_dispatcher_spec.rb +++ b/ruby_event_store/spec/immediate_dispatcher_spec.rb @@ -3,7 +3,7 @@ require "ruby_event_store/spec/scheduler_lint" module RubyEventStore - ::RSpec.describe ImmediateAsyncDispatcher do + ::RSpec.describe ImmediateDispatcher do class MyCustomScheduler def call(klass, record) klass.perform_async(record) @@ -14,7 +14,7 @@ def verify(klass) end end - it_behaves_like :dispatcher, ImmediateAsyncDispatcher.new(scheduler: MyCustomScheduler.new) + it_behaves_like :dispatcher, ImmediateDispatcher.new(scheduler: MyCustomScheduler.new) it_behaves_like :scheduler, MyCustomScheduler.new let(:event) { instance_double(Event) } @@ -23,7 +23,7 @@ def verify(klass) describe "#call" do specify do - dispatcher = ImmediateAsyncDispatcher.new(scheduler: scheduler) + dispatcher = ImmediateDispatcher.new(scheduler: scheduler) handler = spy dispatcher.call(handler, event, record) @@ -34,14 +34,14 @@ def verify(klass) describe "#verify" do specify do - dispatcher = ImmediateAsyncDispatcher.new(scheduler: scheduler) + dispatcher = ImmediateDispatcher.new(scheduler: scheduler) handler = double(perform_async: true) expect(dispatcher.verify(handler)).to eq(true) end specify do - dispatcher = ImmediateAsyncDispatcher.new(scheduler: scheduler) + dispatcher = ImmediateDispatcher.new(scheduler: scheduler) handler = double expect(dispatcher.verify(handler)).to eq(false) diff --git a/ruby_event_store/spec/in_memory_repository_spec.rb b/ruby_event_store/spec/in_memory_repository_spec.rb index 277d91844e..57a0adfd76 100644 --- a/ruby_event_store/spec/in_memory_repository_spec.rb +++ b/ruby_event_store/spec/in_memory_repository_spec.rb @@ -56,7 +56,7 @@ module RubyEventStore end it "publishing with any position to stream with specific position raise an error" do - repository = InMemoryRepository.new(ensure_supported_any_usage: true) + repository = InMemoryRepository.new repository.append_to_stream([event0 = SRecord.new], Stream.new("stream"), ExpectedVersion.auto) expect do @@ -65,7 +65,7 @@ module RubyEventStore end it "publishing with any position to stream with any position does not raise an error" do - repository = InMemoryRepository.new(ensure_supported_any_usage: true) + repository = InMemoryRepository.new repository.append_to_stream([event0 = SRecord.new], Stream.new("stream"), ExpectedVersion.any) expect do @@ -74,7 +74,7 @@ module RubyEventStore end it "publishing with specific position to stream with any position raise an error" do - repository = InMemoryRepository.new(ensure_supported_any_usage: true) + repository = InMemoryRepository.new repository.append_to_stream([event0 = SRecord.new], Stream.new("stream"), ExpectedVersion.any) expect do @@ -83,7 +83,7 @@ module RubyEventStore end it "linking with any position to stream with specific position raise an error" do - repository = InMemoryRepository.new(ensure_supported_any_usage: true) + repository = InMemoryRepository.new repository.append_to_stream([event0 = SRecord.new], Stream.new("stream"), ExpectedVersion.auto) repository.append_to_stream([event1 = SRecord.new], Stream.new("other"), ExpectedVersion.auto) @@ -93,7 +93,7 @@ module RubyEventStore end it "linking with any position to stream with any position does not raise an error" do - repository = InMemoryRepository.new(ensure_supported_any_usage: true) + repository = InMemoryRepository.new repository.append_to_stream([event0 = SRecord.new], Stream.new("stream"), ExpectedVersion.any) repository.append_to_stream([event1 = SRecord.new], Stream.new("other"), ExpectedVersion.auto) @@ -103,7 +103,7 @@ module RubyEventStore end it "linking with specific position to stream with any position raise an error" do - repository = InMemoryRepository.new(ensure_supported_any_usage: true) + repository = InMemoryRepository.new repository.append_to_stream([event0 = SRecord.new], Stream.new("stream"), ExpectedVersion.any) repository.append_to_stream([event1 = SRecord.new], Stream.new("other"), ExpectedVersion.auto) @@ -140,13 +140,5 @@ module RubyEventStore repository.append_to_stream([event1 = SRecord.new], Stream.new("stream"), ExpectedVersion.auto) end.not_to raise_error end - - it "stream position verification is turned off by default" do - repository.append_to_stream([event0 = SRecord.new], Stream.new("stream"), ExpectedVersion.auto) - - expect do - repository.append_to_stream([event1 = SRecord.new], Stream.new("stream"), ExpectedVersion.any) - end.not_to raise_error - end end end diff --git a/ruby_event_store/spec/instrumented_dispatcher_spec.rb b/ruby_event_store/spec/instrumented_dispatcher_spec.rb index aa1c0a4e47..b56060d27d 100644 --- a/ruby_event_store/spec/instrumented_dispatcher_spec.rb +++ b/ruby_event_store/spec/instrumented_dispatcher_spec.rb @@ -6,7 +6,7 @@ module RubyEventStore ::RSpec.describe InstrumentedDispatcher do - it_behaves_like :dispatcher, InstrumentedDispatcher.new(Dispatcher.new, ActiveSupport::Notifications) + it_behaves_like :dispatcher, InstrumentedDispatcher.new(SyncScheduler.new, ActiveSupport::Notifications) describe "#call" do specify "wraps around original implementation" do @@ -23,7 +23,7 @@ module RubyEventStore specify "instruments" do instrumented_dispatcher = InstrumentedDispatcher.new(spy, ActiveSupport::Notifications) - subscribe_to("call.dispatcher.rails_event_store") do |notification_calls| + subscribe_to("call.dispatcher.ruby_event_store") do |notification_calls| event = Object.new record = Object.new subscriber = -> { } @@ -60,7 +60,7 @@ module RubyEventStore end specify "method unknown by instrumentation and unknown by dispatcher" do - some_dispatcher = Dispatcher.new + some_dispatcher = SyncScheduler.new instrumented_dispatcher = InstrumentedDispatcher.new(some_dispatcher, ActiveSupport::Notifications) expect(instrumented_dispatcher).not_to respond_to(:arbitrary_method_name) diff --git a/ruby_event_store/spec/instrumented_repository_spec.rb b/ruby_event_store/spec/instrumented_repository_spec.rb index e793271630..65e50bdb87 100644 --- a/ruby_event_store/spec/instrumented_repository_spec.rb +++ b/ruby_event_store/spec/instrumented_repository_spec.rb @@ -21,10 +21,12 @@ module RubyEventStore specify "instruments" do instrumented_repository = InstrumentedRepository.new(spy, ActiveSupport::Notifications) - subscribe_to("append_to_stream.repository.rails_event_store") do |notification_calls| + subscribe_to("append_to_stream.repository.ruby_event_store") do |notification_calls| instrumented_repository.append_to_stream([record], stream, expected_version) - expect(notification_calls).to eq([{ events: [record], stream: stream }]) + expect(notification_calls).to eq([ + { records: [record], stream: stream } + ]) end end end @@ -40,7 +42,7 @@ module RubyEventStore specify "instruments" do instrumented_repository = InstrumentedRepository.new(spy, ActiveSupport::Notifications) - subscribe_to("link_to_stream.repository.rails_event_store") do |notification_calls| + subscribe_to("link_to_stream.repository.ruby_event_store") do |notification_calls| instrumented_repository.link_to_stream([event_id], stream, expected_version) expect(notification_calls).to eq([{ event_ids: [event_id], stream: stream }]) @@ -59,7 +61,7 @@ module RubyEventStore specify "instruments" do instrumented_repository = InstrumentedRepository.new(spy, ActiveSupport::Notifications) - subscribe_to("delete_stream.repository.rails_event_store") do |notification_calls| + subscribe_to("delete_stream.repository.ruby_event_store") do |notification_calls| instrumented_repository.delete_stream("SomeStream") expect(notification_calls).to eq([{ stream: "SomeStream" }]) @@ -99,7 +101,7 @@ module RubyEventStore specify "instruments" do instrumented_repository = InstrumentedRepository.new(spy, ActiveSupport::Notifications) - subscribe_to("read.repository.rails_event_store") do |notification_calls| + subscribe_to("read.repository.ruby_event_store") do |notification_calls| specification = double instrumented_repository.read(specification) @@ -120,7 +122,7 @@ module RubyEventStore specify "instruments" do instrumented_repository = InstrumentedRepository.new(spy, ActiveSupport::Notifications) - subscribe_to("count.repository.rails_event_store") do |notification_calls| + subscribe_to("count.repository.ruby_event_store") do |notification_calls| specification = double instrumented_repository.count(specification) @@ -140,10 +142,12 @@ module RubyEventStore specify "instruments" do instrumented_repository = InstrumentedRepository.new(spy, ActiveSupport::Notifications) - subscribe_to("update_messages.repository.rails_event_store") do |notification_calls| + subscribe_to("update_messages.repository.ruby_event_store") do |notification_calls| instrumented_repository.update_messages([record]) - expect(notification_calls).to eq([{ messages: [record] }]) + expect(notification_calls).to eq([ + { records: [record] } + ]) end end end @@ -160,7 +164,7 @@ module RubyEventStore specify "instruments" do instrumented_repository = InstrumentedRepository.new(spy, ActiveSupport::Notifications) - subscribe_to("streams_of.repository.rails_event_store") do |notification_calls| + subscribe_to("streams_of.repository.ruby_event_store") do |notification_calls| uuid = SecureRandom.uuid instrumented_repository.streams_of(uuid) diff --git a/ruby_event_store/spec/mappers/default_spec.rb b/ruby_event_store/spec/mappers/default_spec.rb index aa5faae14d..471675d283 100644 --- a/ruby_event_store/spec/mappers/default_spec.rb +++ b/ruby_event_store/spec/mappers/default_spec.rb @@ -55,25 +55,6 @@ module Mappers expect(event_.metadata[:valid_at]).to eq(time) end - specify "#record_to_event its using events class remapping" do - subject = Default.new(events_class_remapping: { "EventNameBeforeRefactor" => "SomethingHappened" }) - record = - Record.new( - event_id: event.event_id, - data: { - some_attribute: 5 - }, - metadata: { - some_meta: 1 - }, - event_type: "EventNameBeforeRefactor", - timestamp: time, - valid_at: time - ) - event_ = subject.record_to_event(record) - expect(event_).to eq(event) - end - specify "metadata keys are symbolized" do record = Record.new( diff --git a/ruby_event_store/spec/mappers/instrumented_mapper_spec.rb b/ruby_event_store/spec/mappers/instrumented_mapper_spec.rb index bcd41dada6..6f91908d67 100644 --- a/ruby_event_store/spec/mappers/instrumented_mapper_spec.rb +++ b/ruby_event_store/spec/mappers/instrumented_mapper_spec.rb @@ -20,9 +20,9 @@ module Mappers specify "instruments" do instrumented_mapper = InstrumentedMapper.new(spy, ActiveSupport::Notifications) - subscribe_to("serialize.mapper.rails_event_store") do |notification_calls| + subscribe_to("event_to_record.mapper.ruby_event_store") do |notification_calls| instrumented_mapper.event_to_record(event) - expect(notification_calls).to eq([{ domain_event: event }]) + expect(notification_calls).to eq([{ event: event }]) end end end @@ -38,7 +38,7 @@ module Mappers specify "instruments" do instrumented_mapper = InstrumentedMapper.new(spy, ActiveSupport::Notifications) - subscribe_to("deserialize.mapper.rails_event_store") do |notification_calls| + subscribe_to("record_to_event.mapper.ruby_event_store") do |notification_calls| instrumented_mapper.record_to_event(record) expect(notification_calls).to eq([{ record: record }]) end diff --git a/ruby_event_store/spec/mappers/json_mapper_spec.rb b/ruby_event_store/spec/mappers/json_mapper_spec.rb deleted file mode 100644 index 0a4f414c3d..0000000000 --- a/ruby_event_store/spec/mappers/json_mapper_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" -require "ruby_event_store/spec/mapper_lint" - -SomethingHappenedJSON = Class.new(RubyEventStore::Event) - -module RubyEventStore - module Mappers - ::RSpec.describe JSONMapper do - specify { expect { JSONMapper.new }.to output(<<~EOW).to_stderr } - Please replace RubyEventStore::Mappers::JSONMapper with RubyEventStore::Mappers::Default - - They're now identical and the former will be removed in next major release. - EOW - end - end -end diff --git a/ruby_event_store/spec/mappers/null_mapper_spec.rb b/ruby_event_store/spec/mappers/null_mapper_spec.rb deleted file mode 100644 index 762411fcfd..0000000000 --- a/ruby_event_store/spec/mappers/null_mapper_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -require "spec_helper" -require "ruby_event_store/spec/mapper_lint" - -module RubyEventStore - module Mappers - ::RSpec.describe NullMapper do - let(:time) { Time.now.utc } - let(:data) { { some_attribute: 5 } } - let(:metadata) { { some_meta: 1 } } - let(:event_id) { SecureRandom.uuid } - let(:event) do - TimeEnrichment.with( - TestEvent.new(data: data, metadata: metadata, event_id: event_id), - timestamp: time, - valid_at: time - ) - end - - it_behaves_like :mapper, NullMapper.new, TimeEnrichment.with(TestEvent.new) - - specify "#event_to_record" do - record = subject.event_to_record(event) - - expect(record.event_id).to eq(event.event_id) - expect(record.data).to eq(event.data) - expect(record.metadata.to_h).to eq(metadata) - expect(record.event_type).to eq("TestEvent") - expect(record.timestamp).to eq(time) - expect(record.valid_at).to eq(time) - end - - specify "#record_to_event" do - record = subject.event_to_record(event) - event_ = subject.record_to_event(record) - - expect(event_).to eq(event) - expect(event_.event_id).to eq(event.event_id) - expect(event_.data).to eq(event.data) - expect(event_.metadata.to_h).to eq(event.metadata.to_h) - expect(event_.event_type).to eq("TestEvent") - expect(event_.metadata[:timestamp]).to eq(time) - expect(event_.metadata[:valid_at]).to eq(time) - end - end - end -end diff --git a/ruby_event_store/spec/mappers/transformation/event_class_remapper_spec.rb b/ruby_event_store/spec/mappers/transformation/event_class_remapper_spec.rb deleted file mode 100644 index 19a83ec1f8..0000000000 --- a/ruby_event_store/spec/mappers/transformation/event_class_remapper_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -require "spec_helper" - -module RubyEventStore - module Mappers - module Transformation - ::RSpec.describe EventClassRemapper do - let(:time) { Time.now.utc } - let(:uuid) { SecureRandom.uuid } - def record(event_type: "TestEvent") - Record.new( - event_id: uuid, - metadata: { - some: "meta" - }, - data: { - some: "value" - }, - event_type: event_type, - timestamp: time, - valid_at: time - ) - end - let(:changeable_record) { record(event_type: "EventNameBeforeRefactor") } - let(:changed_record) { record(event_type: "SomethingHappened") } - let(:class_map) { { "EventNameBeforeRefactor" => "SomethingHappened" } } - - specify "#dump" do - expect(EventClassRemapper.new(class_map).dump(record)).to eq(record) - expect(EventClassRemapper.new(class_map).dump(record)).to eq(record) - end - - specify "#load" do - expect(EventClassRemapper.new(class_map).load(record)).to eq(record) - expect(EventClassRemapper.new(class_map).load(changeable_record)).to eq(changed_record) - end - end - end - end -end diff --git a/ruby_event_store/spec/projection_spec.rb b/ruby_event_store/spec/projection_spec.rb index b004dff155..a12cc0cabb 100644 --- a/ruby_event_store/spec/projection_spec.rb +++ b/ruby_event_store/spec/projection_spec.rb @@ -25,42 +25,11 @@ module RubyEventStore account_balance = Projection - .from_stream(stream_name) - .init(-> { { total: 0 } }) - .when(MoneyDeposited, ->(state, event) { state[:total] += event.data[:amount] }) - .when(MoneyWithdrawn, ->(state, event) { state[:total] -= event.data[:amount] }) - .run(event_store) - expect(account_balance).to eq(total: 25) - end - - specify "reduce events from many streams" do - event_store.append(MoneyDeposited.new(data: { amount: 10 }), stream_name: "Customer$1") - event_store.append(MoneyDeposited.new(data: { amount: 20 }), stream_name: "Customer$2") - event_store.append(MoneyWithdrawn.new(data: { amount: 5 }), stream_name: "Customer$3") - - account_balance = - Projection - .from_stream(%w[Customer$1 Customer$3]) - .init(-> { { total: 0 } }) - .when(MoneyDeposited, ->(state, event) { state[:total] += event.data[:amount] }) - .when(MoneyWithdrawn, ->(state, event) { state[:total] -= event.data[:amount] }) - .run(event_store) - expect(account_balance).to eq(total: 5) - end - - specify "raises proper errors when wrong argument were passed (stream mode)" do - projection = - Projection - .from_stream(%w[Customer$1 Customer$2]) - .init(-> { { total: 0 } }) - .when(MoneyDeposited, ->(state, event) { state[:total] += event.data[:amount] }) - .when(MoneyWithdrawn, ->(state, event) { state[:total] -= event.data[:amount] }) - expect { projection.run(event_store, start: :last) }.to raise_error ArgumentError, - "Start must be an array with event ids" - expect { projection.run(event_store, start: 0.7) }.to raise_error ArgumentError, - "Start must be an array with event ids" - expect { projection.run(event_store, start: [SecureRandom.uuid]) }.to raise_error ArgumentError, - "Start must be an array with event ids" + .init(0) + .on(MoneyDeposited) { |state, event| state += event.data[:amount] } + .on(MoneyWithdrawn) { |state, event| state -= event.data[:amount] } + .call(event_store.read) + expect(account_balance).to eq(25) end specify "take events from all streams" do @@ -71,28 +40,15 @@ module RubyEventStore account_balance = Projection - .from_all_streams - .init(-> { { total: 0 } }) - .when(MoneyDeposited, ->(state, event) { state[:total] += event.data[:amount] }) - .when(MoneyWithdrawn, ->(state, event) { state[:total] -= event.data[:amount] }) - - expect(account_balance.run(event_store)).to eq(total: 1) - end + .init(0) + .on(MoneyDeposited) { |state, event| state += event.data[:amount] } + .on(MoneyWithdrawn) { |state, event| state -= event.data[:amount] } + .call(event_store.read) - specify "raises proper errors when wrong argument were pass (all streams mode)" do - projection = - Projection - .from_all_streams - .init(-> { { total: 0 } }) - .when(MoneyDeposited, ->(state, event) { state[:total] += event.data[:amount] }) - .when(MoneyWithdrawn, ->(state, event) { state[:total] -= event.data[:amount] }) - expect { projection.run(event_store, start: :last) }.to raise_error ArgumentError, "Start must be valid event id" - expect { projection.run(event_store, start: 0.7) }.to raise_error ArgumentError, "Start must be valid event id" - expect { projection.run(event_store, start: [SecureRandom.uuid]) }.to raise_error ArgumentError, - "Start must be valid event id" + expect(account_balance).to eq(1) end - specify "empty hash is default inital state" do + specify "state could be more complex than simple value" do event_store.append( [ MoneyDeposited.new(data: { amount: 10 }), @@ -103,11 +59,10 @@ module RubyEventStore ) stats = - Projection - .from_stream(stream_name) - .when(MoneyDeposited, ->(state, event) { state[:last_deposit] = event.data[:amount] }) - .when(MoneyWithdrawn, ->(state, event) { state[:last_withdrawal] = event.data[:amount] }) - .run(event_store) + Projection.init({}, **{}) + .on(MoneyDeposited) { |state, event| state[:last_deposit] = event.data[:amount]; state } + .on(MoneyWithdrawn) { |state, event| state[:last_withdrawal] = event.data[:amount]; state } + .call(event_store.read.stream(stream_name)) expect(stats).to eq(last_deposit: 20, last_withdrawal: 5) end @@ -119,11 +74,10 @@ module RubyEventStore deposits = Projection - .from_stream(stream_name) - .init(-> { { total: 0 } }) - .when(MoneyDeposited, ->(state, event) { state[:total] += event.data[:amount] }) - .run(event_store) - expect(deposits).to eq(total: 10) + .init(0) + .on(MoneyDeposited) { |state, event| state += event.data[:amount] } + .call(event_store.read.stream(stream_name)) + expect(deposits).to eq(10) end specify "subsrcibe one handler to many events" do @@ -134,35 +88,10 @@ module RubyEventStore cashflow = Projection - .from_stream(stream_name) - .init(-> { { total: 0 } }) - .when([MoneyDeposited, MoneyWithdrawn], ->(state, event) { state[:total] += event.data[:amount] }) - .run(event_store) - expect(cashflow).to eq(total: 12) - end - - specify "subscribe to events" do - deposits = - Projection - .from_stream(stream_name) - .init(-> { { total: 0 } }) - .when(MoneyDeposited, ->(state, event) { state[:total] += event.data[:amount] }) - event_store.subscribe(deposits, to: deposits.handled_events) - event_store.publish( - [MoneyDeposited.new(data: { amount: 10 }), MoneyDeposited.new(data: { amount: 5 })], - stream_name: stream_name - ) - - expect(deposits.current_state).to eq(total: 15) - end - - specify "using default constructor" do - expect { Projection.new(stream_name) }.to raise_error(NoMethodError, /private method `new'/) - end - - specify "at least one stream must be given" do - expect { Projection.from_stream([]) }.to raise_error(ArgumentError, "At least one stream must be given") - expect { Projection.from_stream(nil) }.to raise_error(ArgumentError, "At least one stream must be given") + .init(0) + .on(MoneyDeposited, MoneyWithdrawn) { |state, event| state += event.data[:amount] } + .call(event_store.read.stream(stream_name)) + expect(cashflow).to eq(12) end specify "all events from the stream must be read (starting from begining of the stream)" do @@ -179,12 +108,11 @@ module RubyEventStore balance = Projection - .from_stream(stream_name) - .init(-> { { total: 0 } }) - .when([MoneyDeposited], ->(state, event) { state[:total] += event.data[:amount] }) - .when([MoneyWithdrawn], ->(state, event) { state[:total] -= event.data[:amount] }) - .run(event_store, count: 2) - expect(balance).to eq(total: 14) + .init(0) + .on(MoneyDeposited) { |state, event| state += event.data[:amount] } + .on(MoneyWithdrawn) { |state, event| state -= event.data[:amount] } + .call(event_store.read.stream(stream_name).in_batches(2)) + expect(balance).to eq(14) end specify "all events from the stream must be read (starting from given event)" do @@ -201,12 +129,11 @@ module RubyEventStore balance = Projection - .from_stream(stream_name) - .init(-> { { total: 0 } }) - .when([MoneyDeposited], ->(state, event) { state[:total] += event.data[:amount] }) - .when([MoneyWithdrawn], ->(state, event) { state[:total] -= event.data[:amount] }) - .run(event_store, start: [starting.event_id], count: 2) - expect(balance).to eq(total: 6) + .init(0) + .on(MoneyDeposited) { |state, event| state += event.data[:amount] } + .on(MoneyWithdrawn) { |state, event| state -= event.data[:amount] } + .call(event_store.read.stream(stream_name).from(starting.event_id).in_batches(2)) + expect(balance).to eq(6) end specify "all events from all streams must be read (starting from begining of each stream)" do @@ -218,12 +145,11 @@ module RubyEventStore balance = Projection - .from_all_streams - .init(-> { { total: 0 } }) - .when([MoneyDeposited], ->(state, event) { state[:total] += event.data[:amount] }) - .when([MoneyWithdrawn], ->(state, event) { state[:total] -= event.data[:amount] }) - .run(event_store, count: 2) - expect(balance).to eq(total: 14) + .init(0) + .on(MoneyDeposited) { |state, event| state += event.data[:amount] } + .on(MoneyWithdrawn) { |state, event| state -= event.data[:amount] } + .call(event_store.read.in_batches(2)) + expect(balance).to eq(14) end specify "all events from all streams must be read (starting from given event)" do @@ -235,12 +161,11 @@ module RubyEventStore balance = Projection - .from_all_streams - .init(-> { { total: 0 } }) - .when([MoneyDeposited], ->(state, event) { state[:total] += event.data[:amount] }) - .when([MoneyWithdrawn], ->(state, event) { state[:total] -= event.data[:amount] }) - .run(event_store, start: starting.event_id, count: 2) - expect(balance).to eq(total: 6) + .init(0) + .on(MoneyDeposited) { |state, event| state += event.data[:amount] } + .on(MoneyWithdrawn) { |state, event| state -= event.data[:amount] } + .call(event_store.read.from(starting.event_id).in_batches(2)) + expect(balance).to eq(6) end specify "only events that have handlers must be read" do @@ -260,78 +185,90 @@ module RubyEventStore balance = Projection - .from_all_streams - .init(-> { { total: 0 } }) - .when([MoneyDeposited], ->(state, event) { state[:total] += event.data[:amount] }) - .when([MoneyWithdrawn, MoneyLost], ->(state, event) { state[:total] -= event.data[:amount] }) - .run(event_store, count: 100) - expect(balance).to eq(total: 6) + .init(0) + .on(MoneyDeposited) { |state, event| state += event.data[:amount] } + .on(MoneyWithdrawn, MoneyLost) { |state, event| state -= event.data[:amount] } + .call(event_store.read.in_batches(100)) + expect(balance).to eq(6) end specify do specification = Specification.new(SpecificationReader.new(repository, mapper)) - expected = specification.in_batches(2).of_type([MoneyDeposited, MoneyWithdrawn]).result - expect(repository).to receive(:read).with(expected).and_return([]) + scope = specification.in_batches(2).of_type([MoneyDeposited, MoneyWithdrawn]) + expect(repository).to receive(:read).with(scope.result).and_return([]) Projection - .from_all_streams - .init(-> { { total: 0 } }) - .when(MoneyDeposited, ->(state, event) { state[:total] += event.data[:amount] }) - .when(MoneyWithdrawn, ->(state, event) { state[:total] -= event.data[:amount] }) - .run(event_store, count: 2) + .init(0) + .on(MoneyDeposited) { |state, event| state += event.data[:amount] } + .on(MoneyWithdrawn) { |state, event| state -= event.data[:amount] } + .call(scope) end - specify do - specification = Specification.new(SpecificationReader.new(repository, mapper)) - expected = specification.in_batches(2).of_type([MoneyDeposited, MoneyWithdrawn]).stream("FancyStream").result - expect(repository).to receive(:read).with(expected).and_return([]) + specify "default initial state" do + expect(Projection.init.call([])).to eq(nil) + end - Projection - .from_stream("FancyStream") - .init(-> { { total: 0 } }) - .when(MoneyDeposited, ->(state, event) { state[:total] += event.data[:amount] }) - .when(MoneyWithdrawn, ->(state, event) { state[:total] -= event.data[:amount] }) - .run(event_store, count: 2) + specify "block must be given to on event handlers" do + expect do + Projection.init.on(MoneyDeposited) + end.to raise_error(ArgumentError, "No handler block given") end specify do expect(repository).not_to receive(:read) - - state = Projection.from_all_streams.init(-> { { total: 0 } }).run(event_store, count: 2) - - expect(state).to eq({ total: 0 }) + state = Projection.init.call(event_store.read) + expect(state).to eq(nil) end specify do expect(repository).not_to receive(:read) - state = Projection.from_all_streams.run(event_store) + initial_state = Object.new + state = Projection.init(initial_state).call(event_store.read) - expect(state).to eq({}) + expect(state).to eq(initial_state) end - specify "supports event class remapping" do - event_store = - Client.new( - mapper: Mappers::Default.new(events_class_remapping: { MoneyInvested.to_s => MoneyLost.to_s }) - ) - event_store.append(MoneyInvested.new(data: { amount: 1 })) - - balance = - Projection - .from_all_streams - .init(-> { { total: 0 } }) - .when(MoneyLost, ->(state, event) { state[:total] -= event.data[:amount] }) - .run(event_store) - expect(balance).to eq(total: 0) - - balance = + specify "events with custom event type" do + class CustomResolver + def call(event) + event.resolves_as + end + end + + class Snowflake < Event + def self.resolves_as + "snowflake" + end + + def event_type + "snowflake" + end + end + + class SnowflakeV2 < Event + def self.resolves_as + "snowflake" + end + + def event_type + "snowflake" + end + end + + event_store.append(Snowflake.new(data: { arms: 13 }), stream_name: "snowflake$1") + event_store.append(SnowflakeV2.new(data: { arms: 11 }), stream_name: "snowflake$1") + + state = Projection - .from_all_streams - .init(-> { { total: 0 } }) - .when([MoneyLost, MoneyInvested], ->(state, event) { state[:total] -= event.data[:amount] }) - .run(event_store) - expect(balance).to eq(total: -1) + .init({ snowflake: 0 }, event_type_resolver: CustomResolver.new) + .on(Snowflake, SnowflakeV2) do |state, event| + state[:snowflake] += event.data.fetch(:arms) + state + end + .call(event_store.read.stream("snowflake$1")) + + expect(state).to eq({ snowflake: 24 }) end end end diff --git a/ruby_event_store/spec/dispatcher_spec.rb b/ruby_event_store/spec/sync_scheduler_spec.rb similarity index 62% rename from ruby_event_store/spec/dispatcher_spec.rb rename to ruby_event_store/spec/sync_scheduler_spec.rb index a3bee9aaab..1996e0cd19 100644 --- a/ruby_event_store/spec/dispatcher_spec.rb +++ b/ruby_event_store/spec/sync_scheduler_spec.rb @@ -2,20 +2,20 @@ require "ruby_event_store/spec/dispatcher_lint" module RubyEventStore - ::RSpec.describe Dispatcher do - it_behaves_like :dispatcher, Dispatcher.new + ::RSpec.describe SyncScheduler do + it_behaves_like :dispatcher, SyncScheduler.new let(:event) { instance_double(Event) } let(:record) { instance_double(Record) } let(:handler) { HandlerClass.new } specify "does not allow silly subscribers" do - expect(Dispatcher.new.verify(:symbol)).to eq(false) - expect(Dispatcher.new.verify(Object.new)).to eq(false) + expect(SyncScheduler.new.verify(:symbol)).to eq(false) + expect(SyncScheduler.new.verify(Object.new)).to eq(false) end specify "does not allow class without instance method #call" do klass = Class.new - expect(Dispatcher.new.verify(klass)).to eq(false) + expect(SyncScheduler.new.verify(klass)).to eq(false) end specify "does not allow class without constructor requiring arguments" do @@ -27,24 +27,24 @@ def initialize(something) def call; end end - expect(Dispatcher.new.verify(klass)).to eq(false) + expect(SyncScheduler.new.verify(klass)).to eq(false) end specify "calls subscribed instance" do expect(handler).to receive(:call).with(event) - Dispatcher.new.call(handler, event, record) + SyncScheduler.new.call(handler, event, record) end specify "calls subscribed class" do expect(HandlerClass).to receive(:new).and_return(handler) expect(handler).to receive(:call).with(event) - Dispatcher.new.call(HandlerClass, event, record) + SyncScheduler.new.call(-> (event) { HandlerClass.new.call(event) }, event, record) end specify "allows callable classes and instances" do - expect(Dispatcher.new.verify(HandlerClass)).to eq(true) - expect(Dispatcher.new.verify(HandlerClass.new)).to eq(true) - expect(Dispatcher.new.verify(Proc.new { "yo" })).to eq(true) + expect(SyncScheduler.new.verify(-> { HandlerClass.new })).to eq(true) + expect(SyncScheduler.new.verify(HandlerClass.new)).to eq(true) + expect(SyncScheduler.new.verify(Proc.new { "yo" })).to eq(true) end private