From b22620ed8dfd7889e158be6a6289bdc1aa0c1d70 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 cb269d2..e2c0492 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 = {}) - context.is_a?(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 = {}) + context.is_a?(self) ? 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 eb8b905..37c659e 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 { @@ -125,7 +118,7 @@ module Interactor end describe "#called!" do - let(:context) { Context.build } + let(:context) { context_class.build } let(:instance1) { double(:instance1) } let(:instance2) { double(:instance2) } @@ -140,7 +133,7 @@ module Interactor end describe "#rollback!" do - let(:context) { Context.build } + let(:context) { context_class.build } let(:instance1) { double(:instance1) } let(:instance2) { double(:instance2) } @@ -165,11 +158,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 629d670c66cc1d8cf5aae655db14e485de63a6a2 Mon Sep 17 00:00:00 2001 From: Robert Schulze Date: Mon, 2 Sep 2019 18:35:16 +0200 Subject: [PATCH 2/2] fixing coding style adding some minimalist docs so that rubocop is satisfied --- lib/interactor/context.rb | 51 ++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/lib/interactor/context.rb b/lib/interactor/context.rb index e2c0492..0d4c8f0 100644 --- a/lib/interactor/context.rb +++ b/lib/interactor/context.rb @@ -29,23 +29,27 @@ module Interactor # context # # => # class Context < OpenStruct + # Public: The default mixin any Context implementation should use module Mixin def self.included(receiver) receiver.extend ClassMethods receiver.send :include, InstanceMethods end + # Internal: Context class methods. 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. + # 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: {}) + # Interactor::Context object. If an existing + # Interactor::Context is given, it is simply returned. + # (default: {}) # # Examples # @@ -64,9 +68,10 @@ def build(context = {}) end end + # Internal: Context instance methods. module InstanceMethods - # Public: Whether the Interactor::Context is successful. By default, a new - # context is successful and only changes when explicitly failed. + # 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. # @@ -107,12 +112,12 @@ def failure? @failure || false 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. + # 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. + # 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: {}) @@ -130,15 +135,15 @@ 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 - # 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. + # 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. # @@ -147,9 +152,10 @@ def called!(interactor) _called << interactor 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. + # 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 # @@ -160,7 +166,8 @@ def called!(interactor) # context # # => # # - # Returns true if rolled back successfully or false if already rolled back. + # Returns true if rolled back successfully or false if already rolled + # back. def rollback! return false if @rolled_back _called.reverse_each(&:rollback)