From 63b02bf1d4cce43e6a329609ced275c5c21f018a Mon Sep 17 00:00:00 2001 From: Robert Schulze Date: Wed, 28 Aug 2019 14:43:58 +0200 Subject: [PATCH 1/2] Make the Context class swapable --- lib/interactor.rb | 10 +- lib/interactor/context.rb | 295 +++++++++++++++++--------------- spec/interactor/context_spec.rb | 75 +++++--- 3 files changed, 217 insertions(+), 163 deletions(-) diff --git a/lib/interactor.rb b/lib/interactor.rb index 2423630..6e0babd 100644 --- a/lib/interactor.rb +++ b/lib/interactor.rb @@ -75,6 +75,14 @@ def call(context = {}) def call!(context = {}) new(context).tap(&:run!).context end + + def context_class + @context_class || Interactor::Context + end + + def context_class=(klass) + @context_class = klass + end end # Internal: Initialize an Interactor. @@ -91,7 +99,7 @@ def call!(context = {}) # MyInteractor.new # # => #> def initialize(context = {}) - @context = Context.build(context) + @context = self.class.context_class.build(context) end # Internal: Invoke an interactor instance along with all defined hooks. The diff --git a/lib/interactor/context.rb b/lib/interactor/context.rb index 030ef20..baee28f 100644 --- a/lib/interactor/context.rb +++ b/lib/interactor/context.rb @@ -29,153 +29,166 @@ module Interactor # context # # => # class Context < OpenStruct - # Internal: Initialize an Interactor::Context or preserve an existing one. - # If the argument given is an Interactor::Context, the argument is returned. - # Otherwise, a new Interactor::Context is initialized from the provided - # hash. - # - # The "build" method is used during interactor initialization. - # - # context - A Hash whose key/value pairs are used in initializing a new - # Interactor::Context object. If an existing Interactor::Context - # is given, it is simply returned. (default: {}) - # - # Examples - # - # context = Interactor::Context.build(foo: "bar") - # # => # - # context.object_id - # # => 2170969340 - # context = Interactor::Context.build(context) - # # => # - # context.object_id - # # => 2170969340 - # - # Returns the Interactor::Context. - def self.build(context = {}) - self === context ? context : new(context) - end + module Mixin + def self.included(receiver) + receiver.extend ClassMethods + receiver.send :include, InstanceMethods + end - # Public: Whether the Interactor::Context is successful. By default, a new - # context is successful and only changes when explicitly failed. - # - # The "success?" method is the inverse of the "failure?" method. - # - # Examples - # - # context = Interactor::Context.new - # # => # - # context.success? - # # => true - # context.fail! - # # => Interactor::Failure: # - # context.success? - # # => false - # - # Returns true by default or false if failed. - def success? - !failure? - end + module ClassMethods + # Internal: Initialize an Interactor::Context or preserve an existing one. + # If the argument given is an Interactor::Context, the argument is returned. + # Otherwise, a new Interactor::Context is initialized from the provided + # hash. + # + # The "build" method is used during interactor initialization. + # + # context - A Hash whose key/value pairs are used in initializing a new + # Interactor::Context object. If an existing Interactor::Context + # is given, it is simply returned. (default: {}) + # + # Examples + # + # context = Interactor::Context.build(foo: "bar") + # # => # + # context.object_id + # # => 2170969340 + # context = Interactor::Context.build(context) + # # => # + # context.object_id + # # => 2170969340 + # + # Returns the Interactor::Context. + def build(context = {}) + self === context ? context : new(context) + end + end - # Public: Whether the Interactor::Context has failed. By default, a new - # context is successful and only changes when explicitly failed. - # - # The "failure?" method is the inverse of the "success?" method. - # - # Examples - # - # context = Interactor::Context.new - # # => # - # context.failure? - # # => false - # context.fail! - # # => Interactor::Failure: # - # context.failure? - # # => true - # - # Returns false by default or true if failed. - def failure? - @failure || false - end + module InstanceMethods + # Public: Whether the Interactor::Context is successful. By default, a new + # context is successful and only changes when explicitly failed. + # + # The "success?" method is the inverse of the "failure?" method. + # + # Examples + # + # context = Interactor::Context.new + # # => # + # context.success? + # # => true + # context.fail! + # # => Interactor::Failure: # + # context.success? + # # => false + # + # Returns true by default or false if failed. + def success? + !failure? + end - # Public: Fail the Interactor::Context. Failing a context raises an error - # that may be rescued by the calling interactor. The context is also flagged - # as having failed. - # - # Optionally the caller may provide a hash of key/value pairs to be merged - # into the context before failure. - # - # context - A Hash whose key/value pairs are merged into the existing - # Interactor::Context instance. (default: {}) - # - # Examples - # - # context = Interactor::Context.new - # # => # - # context.fail! - # # => Interactor::Failure: # - # context.fail! rescue false - # # => false - # context.fail!(foo: "baz") - # # => Interactor::Failure: # - # - # Raises Interactor::Failure initialized with the Interactor::Context. - def fail!(context = {}) - context.each { |key, value| modifiable[key.to_sym] = value } - @failure = true - raise Failure, self - end + # Public: Whether the Interactor::Context has failed. By default, a new + # context is successful and only changes when explicitly failed. + # + # The "failure?" method is the inverse of the "success?" method. + # + # Examples + # + # context = Interactor::Context.new + # # => # + # context.failure? + # # => false + # context.fail! + # # => Interactor::Failure: # + # context.failure? + # # => true + # + # Returns false by default or true if failed. + def failure? + @failure || false + end - # Internal: Track that an Interactor has been called. The "called!" method - # is used by the interactor being invoked with this context. After an - # interactor is successfully called, the interactor instance is tracked in - # the context for the purpose of potential future rollback. - # - # interactor - An Interactor instance that has been successfully called. - # - # Returns nothing. - def called!(interactor) - _called << interactor - end + # Public: Fail the Interactor::Context. Failing a context raises an error + # that may be rescued by the calling interactor. The context is also flagged + # as having failed. + # + # Optionally the caller may provide a hash of key/value pairs to be merged + # into the context before failure. + # + # context - A Hash whose key/value pairs are merged into the existing + # Interactor::Context instance. (default: {}) + # + # Examples + # + # context = Interactor::Context.new + # # => # + # context.fail! + # # => Interactor::Failure: # + # context.fail! rescue false + # # => false + # context.fail!(foo: "baz") + # # => Interactor::Failure: # + # + # Raises Interactor::Failure initialized with the Interactor::Context. + def fail!(context = {}) + context.each { |key, value| self.send("#{key}=", value) } + @failure = true + raise Failure, self + end - # Public: Roll back the Interactor::Context. Any interactors to which this - # context has been passed and which have been successfully called are asked - # to roll themselves back by invoking their "rollback" instance methods. - # - # Examples - # - # context = MyInteractor.call(foo: "bar") - # # => # - # context.rollback! - # # => true - # context - # # => # - # - # Returns true if rolled back successfully or false if already rolled back. - def rollback! - return false if @rolled_back - _called.reverse_each(&:rollback) - @rolled_back = true - end + # Internal: Track that an Interactor has been called. The "called!" method + # is used by the interactor being invoked with this context. After an + # interactor is successfully called, the interactor instance is tracked in + # the context for the purpose of potential future rollback. + # + # interactor - An Interactor instance that has been successfully called. + # + # Returns nothing. + def called!(interactor) + _called << interactor + end - # Internal: An Array of successfully called Interactor instances invoked - # against this Interactor::Context instance. - # - # Examples - # - # context = Interactor::Context.new - # # => # - # context._called - # # => [] - # - # context = MyInteractor.call(foo: "bar") - # # => # - # context._called - # # => [#>] - # - # Returns an Array of Interactor instances or an empty Array. - def _called - @called ||= [] + # Public: Roll back the Interactor::Context. Any interactors to which this + # context has been passed and which have been successfully called are asked + # to roll themselves back by invoking their "rollback" instance methods. + # + # Examples + # + # context = MyInteractor.call(foo: "bar") + # # => # + # context.rollback! + # # => true + # context + # # => # + # + # Returns true if rolled back successfully or false if already rolled back. + def rollback! + return false if @rolled_back + _called.reverse_each(&:rollback) + @rolled_back = true + end + + # Internal: An Array of successfully called Interactor instances invoked + # against this Interactor::Context instance. + # + # Examples + # + # context = Interactor::Context.new + # # => # + # context._called + # # => [] + # + # context = MyInteractor.call(foo: "bar") + # # => # + # context._called + # # => [#>] + # + # Returns an Array of Interactor instances or an empty Array. + def _called + @called ||= [] + end + end end + + include Mixin end end diff --git a/spec/interactor/context_spec.rb b/spec/interactor/context_spec.rb index 1769172..572d961 100644 --- a/spec/interactor/context_spec.rb +++ b/spec/interactor/context_spec.rb @@ -1,25 +1,18 @@ module Interactor - describe Context do + shared_examples "context" do describe ".build" do it "converts the given hash to a context" do - context = Context.build(foo: "bar") + context = context_class.build(foo: "bar") - expect(context).to be_a(Context) + expect(context).to be_a(context_class) expect(context.foo).to eq("bar") end - it "builds an empty context if no hash is given" do - context = Context.build - - expect(context).to be_a(Context) - expect(context.send(:table)).to eq({}) - end - it "doesn't affect the original hash" do hash = {foo: "bar"} - context = Context.build(hash) + context = context_class.build(hash) - expect(context).to be_a(Context) + expect(context).to be_a(context_class) expect { context.foo = "baz" }.not_to change { @@ -28,10 +21,10 @@ module Interactor end it "preserves an already built context" do - context1 = Context.build(foo: "bar") - context2 = Context.build(context1) + context1 = context_class.build(foo: "bar") + context2 = context_class.build(context1) - expect(context2).to be_a(Context) + expect(context2).to be_a(context_class) expect { context2.foo = "baz" }.to change { @@ -41,7 +34,7 @@ module Interactor end describe "#success?" do - let(:context) { Context.build } + let(:context) { context_class.build } it "is true by default" do expect(context.success?).to eq(true) @@ -49,7 +42,7 @@ module Interactor end describe "#failure?" do - let(:context) { Context.build } + let(:context) { context_class.build } it "is false by default" do expect(context.failure?).to eq(false) @@ -57,7 +50,7 @@ module Interactor end describe "#fail!" do - let(:context) { Context.build(foo: "bar") } + let(:context) { context_class.build(foo: "bar") } it "sets success to false" do expect { @@ -153,7 +146,7 @@ module Interactor end describe "#called!" do - let(:context) { Context.build } + let(:context) { context_class.build } let(:instance1) { double(:instance1) } let(:instance2) { double(:instance2) } @@ -168,7 +161,7 @@ module Interactor end describe "#rollback!" do - let(:context) { Context.build } + let(:context) { context_class.build } let(:instance1) { double(:instance1) } let(:instance2) { double(:instance2) } @@ -193,11 +186,51 @@ module Interactor end describe "#_called" do - let(:context) { Context.build } + let(:context) { context_class.build } it "is empty by default" do expect(context._called).to eq([]) end end end + + describe Context do + it_behaves_like "context" do + let(:context_class) { Context } + + it "builds an empty context if no hash is given" do + context = context_class.build + + expect(context).to be_a(context_class) + expect(context.send(:table)).to eq({}) + end + end + end + + describe "Overwriting Context" do + it_behaves_like "context" do + let(:context_class) do + Class.new do + include Context::Mixin + + attr_accessor :foo + + def initialize(foo: nil) + @foo = foo + end + + def to_h + { foo: foo } + end + end + end + + it "builds the default context if no hash is given" do + context = context_class.build + + expect(context).to be_a(context_class) + expect(context.to_h).to eq(foo: nil) + end + end + end end From c72ab2eb48fa734f30d4c469f0fec88774743335 Mon Sep 17 00:00:00 2001 From: Robert Schulze Date: Wed, 28 Aug 2019 15:04:37 +0200 Subject: [PATCH 2/2] fix coding styles --- interactor.gemspec | 14 +++++++------- lib/interactor/context.rb | 4 ++-- spec/interactor/context_spec.rb | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/interactor.gemspec b/interactor.gemspec index ca108d4..0cb25dc 100644 --- a/interactor.gemspec +++ b/interactor.gemspec @@ -1,17 +1,17 @@ require "English" Gem::Specification.new do |spec| - spec.name = "interactor" + spec.name = "interactor" spec.version = "3.1.1" - spec.author = "Collective Idea" - spec.email = "info@collectiveidea.com" + spec.author = "Collective Idea" + spec.email = "info@collectiveidea.com" spec.description = "Interactor provides a common interface for performing complex user interactions." - spec.summary = "Simple interactor implementation" - spec.homepage = "https://github.com/collectiveidea/interactor" - spec.license = "MIT" + spec.summary = "Simple interactor implementation" + spec.homepage = "https://github.com/collectiveidea/interactor" + spec.license = "MIT" - spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) + spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) spec.test_files = spec.files.grep(/^spec/) spec.add_development_dependency "bundler" diff --git a/lib/interactor/context.rb b/lib/interactor/context.rb index baee28f..13711f3 100644 --- a/lib/interactor/context.rb +++ b/lib/interactor/context.rb @@ -31,7 +31,7 @@ module Interactor class Context < OpenStruct module Mixin def self.included(receiver) - receiver.extend ClassMethods + receiver.extend ClassMethods receiver.send :include, InstanceMethods end @@ -130,7 +130,7 @@ def failure? # # Raises Interactor::Failure initialized with the Interactor::Context. def fail!(context = {}) - context.each { |key, value| self.send("#{key}=", value) } + context.each { |key, value| send("#{key}=", value) } @failure = true raise Failure, self end diff --git a/spec/interactor/context_spec.rb b/spec/interactor/context_spec.rb index 572d961..6c6e93a 100644 --- a/spec/interactor/context_spec.rb +++ b/spec/interactor/context_spec.rb @@ -220,7 +220,7 @@ def initialize(foo: nil) end def to_h - { foo: foo } + {foo: foo} end end end