diff --git a/lib/solidus_support.rb b/lib/solidus_support.rb index c649f1d..f51ac89 100644 --- a/lib/solidus_support.rb +++ b/lib/solidus_support.rb @@ -3,6 +3,7 @@ require 'solidus_support/version' require 'solidus_support/migration' require 'solidus_support/engine_extensions' +require 'solidus_support/legacy_event_compat' require 'solidus_core' module SolidusSupport diff --git a/lib/solidus_support/engine_extensions.rb b/lib/solidus_support/engine_extensions.rb index 54065d2..9f9aa13 100644 --- a/lib/solidus_support/engine_extensions.rb +++ b/lib/solidus_support/engine_extensions.rb @@ -32,7 +32,7 @@ def activate # This allows to add event subscribers to extensions without explicitly subscribing them, # similarly to what happens in Solidus core. def load_solidus_subscribers_from(path) - if defined? Spree::Event + if SolidusSupport::LegacyEventCompat.using_legacy? path.glob("**/*_subscriber.rb") do |subscriber_path| require_dependency(subscriber_path) end diff --git a/lib/solidus_support/legacy_event_compat.rb b/lib/solidus_support/legacy_event_compat.rb new file mode 100644 index 0000000..0a55c27 --- /dev/null +++ b/lib/solidus_support/legacy_event_compat.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'solidus_support/legacy_event_compat/bus' +require 'solidus_support/legacy_event_compat/subscriber' + +module SolidusSupport + # Compatibility middleman for {Spree::Event} and {Spree::Bus} + # + # Solidus v3.2 changed to use [Omnes](https://github.com/nebulab/omnes) as the + # backbone for event-driven behavior (see {Spree::Bus}) by default. Before + # that, a custom adapter based on {ActiveSupport::Notifications} was used (see + # {Spree::Event}. Both systems are still supported on v3.2. + # + # This module provides compatibility support so that extensions can easily + # target both systems regardless of the underlying circumstances: + # + # - Solidus v3.2 with the new system. + # - Solidus v3.2 with the legacy system. + # - Solidus v2.9 to v3.1, when only {Spree::Event} existed. + # - Possible future versions of Solidus, whether the legacy system is + # eventually removed or not. + module LegacyEventCompat + # Returns whether the application is using the legacy event system + # + # @return [Boolean] + def self.using_legacy? + legacy_present? && + (legacy_alone? || + legacy_chosen?) + end + + def self.legacy_present? + defined?(Spree::Event) + end + private_class_method :legacy_present? + + def self.legacy_alone? + !Spree::Config.respond_to?(:use_legacy_events) + end + private_class_method :legacy_alone? + + def self.legacy_chosen? + Spree::Config.use_legacy_events + end + private_class_method :legacy_chosen? + end +end diff --git a/lib/solidus_support/legacy_event_compat/bus.rb b/lib/solidus_support/legacy_event_compat/bus.rb new file mode 100644 index 0000000..1f67ed4 --- /dev/null +++ b/lib/solidus_support/legacy_event_compat/bus.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module SolidusSupport + module LegacyEventCompat + # Compatibility for some event-driven operations + module Bus + # Publication of an event + # + # If extensions want to support the legacy sytem, they need to use a + # compatible API. That means it's not possible to publish an instance as + # event, which is something supported by Omnes but not the legacy adapter. + # Instead, a payload can be given. E.g.: + # + # ``` + # SolidusSupport::LegacyEventCompat::Bus.publish(:foo, bar: :baz) + # ``` + # + # Legacy subscribers will receive an + # `ActiveSupport::Notifications::Fanout`, while omnes subscribers will get + # an `Omnes::UnstructuredEvent`. Both instances are compatible as they + # implement a `#payload` method. + # + # @param event_name [Symbol] + # @param payload [Hash] + def self.publish(event_name, **payload) + if SolidusSupport::LegacyEventCompat.using_legacy? + Spree::Event.fire(event_name, payload) + else + Spree::Bus.publish(event_name, **payload, caller_location: caller_locations(1)[0]) + end + end + end + end +end diff --git a/lib/solidus_support/legacy_event_compat/subscriber.rb b/lib/solidus_support/legacy_event_compat/subscriber.rb new file mode 100644 index 0000000..9090141 --- /dev/null +++ b/lib/solidus_support/legacy_event_compat/subscriber.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +begin + require "omnes" +rescue LoadError +end + +module SolidusSupport + module LegacyEventCompat + # Compatibility for subscriber modules + # + # Thanks to this module, extensions can create legacy subscriber modules + # (see {Spree::Event::Subscriber}) and translate them automatically to an + # {Omnes::Subscriber}). E.g.: + # + # ``` + # module MyExtension + # module MySubscriber + # include Spree::Event::Subscriber + # include SolidusSupport::LegacyEventCompat::Subscriber + # + # event_action :order_finalized + # + # def order_finalized(event) + # event.payload[:order].do_something + # end + # end + # end + # + # MyExtension::MySubscriber.omnes_subscriber.subscribe_to(Spree::Bus) + # ``` + # + # The generated omnes subscriptions will call the corresponding legacy + # subscriber method with the omnes event. It'll compatible as long as the + # omnes event responds to the `#payload` method (see + # {Omnes::UnstructuredEvent}). + module Subscriber + # @api private + ADAPTER = lambda do |legacy_subscriber, legacy_subscriber_method, _omnes_subscriber, omnes_event| + legacy_subscriber.send(legacy_subscriber_method, omnes_event) + end + + def self.included(legacy_subscriber) + legacy_subscriber.define_singleton_method(:omnes_subscriber) do + @omnes_subscriber ||= Class.new.include(::Omnes::Subscriber).tap do |subscriber| + legacy_subscriber.event_actions.each do |(legacy_subscriber_method, event_name)| + subscriber.handle(event_name.to_sym, with: ADAPTER.curry[legacy_subscriber, legacy_subscriber_method]) + end + end.new + end + end + end + end +end diff --git a/solidus_support.gemspec b/solidus_support.gemspec index f712d93..a45f9de 100644 --- a/solidus_support.gemspec +++ b/solidus_support.gemspec @@ -28,4 +28,5 @@ Gem::Specification.new do |s| s.add_development_dependency 'rubocop' s.add_development_dependency 'rubocop-rspec' s.add_development_dependency 'solidus_dev_support' + s.add_development_dependency 'omnes', '~> 0.2.2' end diff --git a/spec/solidus_support/legacy_event_compat/bus_spec.rb b/spec/solidus_support/legacy_event_compat/bus_spec.rb new file mode 100644 index 0000000..1479a5e --- /dev/null +++ b/spec/solidus_support/legacy_event_compat/bus_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +RSpec.describe SolidusSupport::LegacyEventCompat::Bus do + describe '#publish' do + if SolidusSupport::LegacyEventCompat.using_legacy? + it 'forwards to Spree::Event' do + box = nil + subscription = Spree::Event.subscribe(:foo) { |event| box = event.payload[:bar] } + + described_class.publish(:foo, bar: :baz) + + expect(box).to be(:baz) + ensure + Spree::Event.unsubscribe(subscription) + end + else + it 'forwards to Spree::Bus' do + box = nil + Spree::Bus.register(:foo) + subscription = Spree::Bus.subscribe(:foo) { |event| box = event.payload[:bar] } + + described_class.publish(:foo, bar: :baz) + + expect(box).to be(:baz) + ensure + Spree::Bus.unsubscribe(subscription) + Spree::Bus.registry.unregister(:foo) + end + end + end +end diff --git a/spec/solidus_support/legacy_event_compat/legacy_event_compat_spec.rb b/spec/solidus_support/legacy_event_compat/legacy_event_compat_spec.rb new file mode 100644 index 0000000..31dd391 --- /dev/null +++ b/spec/solidus_support/legacy_event_compat/legacy_event_compat_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'omnes' + +RSpec.describe SolidusSupport::LegacyEventCompat::Subscriber do + subject { Module.new.include(Spree::Event::Subscriber).include(described_class) } + + describe '#omnes_subscriber' do + it 'returns an Omnes::Subscriber' do + subject.module_eval do + event_action :foo + + def foo(_event); end + end + + expect(subject.omnes_subscriber.is_a?(Omnes::Subscriber)).to be(true) + end + + it 'adds single-event definitions matching legacy event actions' do + subject.module_eval do + event_action :foo + + def foo(_event); end + end + bus = Omnes::Bus.new + bus.register(:foo) + + subscriptions = subject.omnes_subscriber.subscribe_to(bus) + + event = Struct.new(:omnes_event_name).new(:foo) + expect(subscriptions.first.matches?(event)).to be(true) + end + + it 'coerces event names given as Strings' do + subject.module_eval do + event_action 'foo' + + def foo(_event); end + end + bus = Omnes::Bus.new + bus.register(:foo) + + subscriptions = subject.omnes_subscriber.subscribe_to(bus) + + event = Struct.new(:omnes_event_name).new(:foo) + expect(subscriptions.first.matches?(event)).to be(true) + end + + it 'executes legacy event action methods as handlers with the omnes event' do + subject.module_eval do + event_action :foo + + def foo(event) + event[:bar] + end + end + bus = Omnes::Bus.new + bus.register(:foo) + + subscriptions = subject.omnes_subscriber.subscribe_to(bus) + + expect( + bus.publish(:foo, bar: :baz).executions.first.result + ).to be(:baz) + end + + it 'distingish when event name is given explicitly' do + subject.module_eval do + event_action :foo, event_name: :bar + + def foo(_event) + :bar + end + end + bus = Omnes::Bus.new + bus.register(:bar) + + subscriptions = subject.omnes_subscriber.subscribe_to(bus) + + expect( + bus.publish(:bar).executions.first.result + ).to be(:bar) + end + + it "returns the same omnes subscriber instance if called again" do + expect(subject.omnes_subscriber).to be(subject.omnes_subscriber) + end + + it "doesn't fail when no event action has been defined" do + expect { subject.omnes_subscriber }.not_to raise_error + end + end +end