From d79db0a658d5c9c5f6470fca1f920b38a1433f94 Mon Sep 17 00:00:00 2001 From: Mark Burns Date: Fri, 29 Dec 2023 16:36:23 +0000 Subject: [PATCH 1/5] add missing specs --- spec/lib/interactify_spec.rb | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/spec/lib/interactify_spec.rb b/spec/lib/interactify_spec.rb index 166f3dc..05105a7 100644 --- a/spec/lib/interactify_spec.rb +++ b/spec/lib/interactify_spec.rb @@ -5,6 +5,41 @@ expect(Interactify::VERSION).not_to be nil end + describe ".validate_app" do + before do + wiring = instance_double(Interactify::Wiring, validate_app: 'ok') + + expect(Interactify::Wiring) + .to receive(:new) + .with(root: Interactify.configuration.root, ignore:) + .and_return(wiring) + end + + context "with an ignore" do + let(:ignore) { %w[foo bar] } + + it "validates the app" do + expect(Interactify.validate_app(ignore:)).to eq("ok") + end + end + + context "with nil ignore" do + let(:ignore) { nil } + + it "validates the app" do + expect(Interactify.validate_app(ignore:)).to eq("ok") + end + end + + context "with empty ignore" do + let(:ignore) { [] } + + it "validates the app" do + expect(Interactify.validate_app(ignore:)).to eq("ok") + end + end + end + describe ".reset" do context "with a before raise hook" do before do From 14bf9a236ef277854484b5206e8ef4b5d00eaeb1 Mon Sep 17 00:00:00 2001 From: Mark Burns Date: Fri, 29 Dec 2023 16:36:56 +0000 Subject: [PATCH 2/5] rename to success_arg etc --- lib/interactify/dsl.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/interactify/dsl.rb b/lib/interactify/dsl.rb index 6a0065a..c06ce06 100644 --- a/lib/interactify/dsl.rb +++ b/lib/interactify/dsl.rb @@ -22,12 +22,14 @@ def each(plural_resource_name, *each_loop_klasses) ) end - def if(condition, succcess_interactor, failure_interactor = nil) + def if(condition, succcess_interactor, failure_interactor = nil); end + + def if(condition, succcess_arg, failure_arg = nil) IfInteractor.attach_klass( self, condition, - succcess_interactor, - failure_interactor + succcess_arg, + failure_arg ) end From 7cbd69a3fc5979a5b783f695a913f6eb46026c36 Mon Sep 17 00:00:00 2001 From: Mark Burns Date: Fri, 29 Dec 2023 16:37:06 +0000 Subject: [PATCH 3/5] fix constant name --- lib/interactify.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/interactify.rb b/lib/interactify.rb index 3712d60..579f990 100644 --- a/lib/interactify.rb +++ b/lib/interactify.rb @@ -62,7 +62,7 @@ module Interactify class << self def validate_app(ignore: []) - Interactify::InteractorWiring.new(root: Interactify.configuration.root, ignore:).validate_app + Interactify::Wiring.new(root: Interactify.configuration.root, ignore:).validate_app end def reset From db15d604e79abd106c8a882338cf54b0d3bc9f37 Mon Sep 17 00:00:00 2001 From: Mark Burns Date: Fri, 29 Dec 2023 17:44:04 +0000 Subject: [PATCH 4/5] add support for then else in if method syntax --- lib/interactify/dsl.rb | 12 +++-- lib/interactify/dsl/if_interactor.rb | 80 +++++++++++++++++++--------- spec/lib/interactify/dsl_spec.rb | 79 +++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 28 deletions(-) create mode 100644 spec/lib/interactify/dsl_spec.rb diff --git a/lib/interactify/dsl.rb b/lib/interactify/dsl.rb index c06ce06..cf6a02d 100644 --- a/lib/interactify/dsl.rb +++ b/lib/interactify/dsl.rb @@ -22,14 +22,18 @@ def each(plural_resource_name, *each_loop_klasses) ) end - def if(condition, succcess_interactor, failure_interactor = nil); end + def if(condition, success_arg, failure_arg = nil) + then_else = if success_arg.is_a?(Hash) && failure_arg.nil? + success_arg.slice(:then, :else) + else + { then: success_arg, else: failure_arg } + end - def if(condition, succcess_arg, failure_arg = nil) IfInteractor.attach_klass( self, condition, - succcess_arg, - failure_arg + then_else[:then], + then_else[:else] ) end diff --git a/lib/interactify/dsl/if_interactor.rb b/lib/interactify/dsl/if_interactor.rb index b7300b7..401a2db 100644 --- a/lib/interactify/dsl/if_interactor.rb +++ b/lib/interactify/dsl/if_interactor.rb @@ -5,18 +5,26 @@ module Interactify module Dsl class IfInteractor - attr_reader :condition, :success_interactor, :failure_interactor, :evaluating_receiver + attr_reader :condition, :evaluating_receiver def self.attach_klass(evaluating_receiver, condition, succcess_interactor, failure_interactor) ifable = new(evaluating_receiver, condition, succcess_interactor, failure_interactor) ifable.attach_klass end - def initialize(evaluating_receiver, condition, succcess_interactor, failure_interactor) + def initialize(evaluating_receiver, condition, succcess_arg, failure_arg) @evaluating_receiver = evaluating_receiver @condition = condition - @success_interactor = succcess_interactor - @failure_interactor = failure_interactor + @success_arg = succcess_arg + @failure_arg = failure_arg + end + + def success_interactor + @success_interactor ||= build_chain(@success_arg, true) + end + + def failure_interactor + @failure_interactor ||= build_chain(@failure_arg, false) end # allows us to dynamically create an interactor chain @@ -26,31 +34,38 @@ def initialize(evaluating_receiver, condition, succcess_interactor, failure_inte def klass this = self - Class.new do - include Interactor - include Interactor::Contracts + klass_basis.tap do |k| + k.instance_eval do + expects do + required(this.condition) unless this.condition.is_a?(Proc) + end - expects do - required(this.condition) unless this.condition.is_a?(Proc) - end + define_singleton_method(:source_location) do + const_source_location this.evaluating_receiver.to_s # [file, line] + end - define_singleton_method(:source_location) do - const_source_location this.evaluating_receiver.to_s # [file, line] - end + define_method(:run!) do + result = this.condition.is_a?(Proc) ? this.condition.call(context) : context.send(this.condition) + interactor = result ? this.success_interactor : this.failure_interactor + interactor&.respond_to?(:call!) ? interactor.call!(context) : interactor&.call(context) + end - define_method(:run!) do - result = this.condition.is_a?(Proc) ? this.condition.call(context) : context.send(this.condition) - interactor = result ? this.success_interactor : this.failure_interactor - interactor&.respond_to?(:call!) ? interactor.call!(context) : interactor&.call(context) - end - - define_method(:inspect) do - "<#{this.namespace}::#{this.if_klass_name} #{this.condition} ? #{this.success_interactor} : #{this.failure_interactor}>" + define_method(:inspect) do + "<#{this.namespace}::#{this.if_klass_name} #{this.condition} ? #{this.success_interactor} : #{this.failure_interactor}>" + end end end end # rubocop:enable all + # so we have something to attach subclasses to during building + # of the outer class, before we finalize the outer If class + def klass_basis + @klass_basis ||= Class.new do + include Interactify + end + end + def attach_klass name = if_klass_name namespace.const_set(name, klass) @@ -62,10 +77,27 @@ def namespace end def if_klass_name - prefix = condition.is_a?(Proc) ? "Proc" : condition - prefix = "If#{prefix.to_s.camelize}" + @if_klass_name ||= + begin + prefix = condition.is_a?(Proc) ? "Proc" : condition + prefix = "If#{prefix.to_s.camelize}" + + UniqueKlassName.for(namespace, prefix) + end + end - UniqueKlassName.for(namespace, prefix) + private + + def build_chain(arg, truthiness) + return if arg.nil? + + case arg + when Array + name = "If#{condition.to_s.camelize}#{truthiness ? "IsTruthy" : "IsFalsey"}" + klass_basis.chain(name, *arg) + else + arg + end end end end diff --git a/spec/lib/interactify/dsl_spec.rb b/spec/lib/interactify/dsl_spec.rb new file mode 100644 index 0000000..0f8c83b --- /dev/null +++ b/spec/lib/interactify/dsl_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +RSpec.describe Interactify::Dsl do + self::Slot = Class.new do + extend Interactify::Dsl + end + + let(:slot) { self.class::Slot } + + describe ".if" do + context "with condition, success, and failure arguments" do + let(:return_value) { "some return value" } + + it "passes them through to the IfInteractor" do + allow(described_class::IfInteractor).to receive(:attach_klass).and_return(return_value) + + expect(slot.if(:condition, :success, :failure)).to eq(return_value) + + expect(described_class::IfInteractor).to have_received(:attach_klass).with( + slot, + :condition, + :success, + :failure + ) + end + + let(:on_success1) { ->(ctx) { + ctx.success1 = true } + } + let(:on_success2) { ->(ctx) { ctx.success2 = true } } + + let(:on_failure1) { ->(ctx) { ctx.success1 = false } } + let(:on_failure2) { ->(ctx) { ctx.success2 = false } } + + context "when the success and failure arguments are arrays" do + it "chains the interactors" do + klass = slot.if( + :condition, + [on_success1, on_success2], + [on_failure1, on_failure2] + ) + + expect(klass.ancestors).to include Interactor + expect(klass.ancestors).to include Interactor::Contracts + + result = klass.call!(condition: true) + expect(result.success1).to eq(true) + expect(result.success2).to eq(true) + + result = klass.call!(condition: false) + expect(result.success1).to eq(false) + expect(result.success2).to eq(false) + end + end + + context "when using hash then, else syntax" do + it "chains the interactors" do + klass = slot.if( + :condition, + then: [on_success1, on_success2], + else: [on_failure1, on_failure2] + ) + + expect(klass.ancestors).to include Interactor + expect(klass.ancestors).to include Interactor::Contracts + + result = klass.call!(condition: true) + expect(result.success1).to eq(true) + expect(result.success2).to eq(true) + + result = klass.call!(condition: false) + expect(result.success1).to eq(false) + expect(result.success2).to eq(false) + end + end + + end + end +end From 398f8bc3982adb0587916e57ac85f69e0fa35d4a Mon Sep 17 00:00:00 2001 From: Mark Burns Date: Fri, 29 Dec 2023 18:17:40 +0000 Subject: [PATCH 5/5] extract IfKlass --- lib/interactify/dsl/each_chain.rb | 2 +- lib/interactify/dsl/if_interactor.rb | 29 ++-------- lib/interactify/dsl/if_klass.rb | 80 ++++++++++++++++++++++++++++ spec/lib/interactify/dsl_spec.rb | 9 ++-- spec/lib/interactify_spec.rb | 2 +- 5 files changed, 90 insertions(+), 32 deletions(-) create mode 100644 lib/interactify/dsl/if_klass.rb diff --git a/lib/interactify/dsl/each_chain.rb b/lib/interactify/dsl/each_chain.rb index 06a4885..b937a21 100644 --- a/lib/interactify/dsl/each_chain.rb +++ b/lib/interactify/dsl/each_chain.rb @@ -26,7 +26,7 @@ def klass this = self Class.new do # class SomeNamespace::EachPackage - include Interactify # include Interactify + include Interactify # include Interactify expects do # expects do required(this.plural_resource_name) # required(:packages) diff --git a/lib/interactify/dsl/if_interactor.rb b/lib/interactify/dsl/if_interactor.rb index 401a2db..74354f5 100644 --- a/lib/interactify/dsl/if_interactor.rb +++ b/lib/interactify/dsl/if_interactor.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "interactify/dsl/unique_klass_name" +require "interactify/dsl/if_klass" module Interactify module Dsl @@ -30,33 +31,9 @@ def failure_interactor # allows us to dynamically create an interactor chain # that iterates over the packages and # uses the passed in each_loop_klasses - # rubocop:disable all def klass - this = self - - klass_basis.tap do |k| - k.instance_eval do - expects do - required(this.condition) unless this.condition.is_a?(Proc) - end - - define_singleton_method(:source_location) do - const_source_location this.evaluating_receiver.to_s # [file, line] - end - - define_method(:run!) do - result = this.condition.is_a?(Proc) ? this.condition.call(context) : context.send(this.condition) - interactor = result ? this.success_interactor : this.failure_interactor - interactor&.respond_to?(:call!) ? interactor.call!(context) : interactor&.call(context) - end - - define_method(:inspect) do - "<#{this.namespace}::#{this.if_klass_name} #{this.condition} ? #{this.success_interactor} : #{this.failure_interactor}>" - end - end - end + IfKlass.new(self).klass end - # rubocop:enable all # so we have something to attach subclasses to during building # of the outer class, before we finalize the outer If class @@ -93,7 +70,7 @@ def build_chain(arg, truthiness) case arg when Array - name = "If#{condition.to_s.camelize}#{truthiness ? "IsTruthy" : "IsFalsey"}" + name = "If#{condition.to_s.camelize}#{truthiness ? 'IsTruthy' : 'IsFalsey'}" klass_basis.chain(name, *arg) else arg diff --git a/lib/interactify/dsl/if_klass.rb b/lib/interactify/dsl/if_klass.rb new file mode 100644 index 0000000..5632fb0 --- /dev/null +++ b/lib/interactify/dsl/if_klass.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module Interactify + module Dsl + class IfKlass + attr_reader :if_builder + + def initialize(if_builder) + @if_builder = if_builder + end + + def klass + attach_expectations + attach_source_location + attach_run! + attach_inspect + + if_builder.klass_basis + end + + def run!(context) + result = condition.is_a?(Proc) ? condition.call(context) : context.send(condition) + + interactor = result ? success_interactor : failure_interactor + interactor.respond_to?(:call!) ? interactor.call!(context) : interactor&.call(context) + end + + private + + def attach_source_location + attach do |_klass, this| + define_singleton_method(:source_location) do # def self.source_location + const_source_location this.evaluating_receiver.to_s # [file, line] + end + end + end + + def attach_expectations + attach do |klass, this| + klass.expects do + required(this.condition) unless this.condition.is_a?(Proc) + end + end + end + + def attach_run! + this = self + + attach_method(:run!) do + this.run!(context) + end + end + + delegate :condition, :success_interactor, :failure_interactor, to: :if_builder + + def attach_inspect + this = if_builder + + attach_method(:inspect) do + name = "#{this.namespace}::#{this.if_klass_name}" + "<#{name} #{this.condition} ? #{this.success_interactor} : #{this.failure_interactor}>" + end + end + + def attach_method(name, &) + attach do |klass, _this| + klass.define_method(name, &) + end + end + + def attach + this = if_builder + + this.klass_basis.instance_eval do + yield self, this + end + end + end + end +end diff --git a/spec/lib/interactify/dsl_spec.rb b/spec/lib/interactify/dsl_spec.rb index 0f8c83b..66cd666 100644 --- a/spec/lib/interactify/dsl_spec.rb +++ b/spec/lib/interactify/dsl_spec.rb @@ -24,9 +24,11 @@ ) end - let(:on_success1) { ->(ctx) { - ctx.success1 = true } - } + let(:on_success1) do + lambda { |ctx| + ctx.success1 = true + } + end let(:on_success2) { ->(ctx) { ctx.success2 = true } } let(:on_failure1) { ->(ctx) { ctx.success1 = false } } @@ -73,7 +75,6 @@ expect(result.success2).to eq(false) end end - end end end diff --git a/spec/lib/interactify_spec.rb b/spec/lib/interactify_spec.rb index 05105a7..9f150c8 100644 --- a/spec/lib/interactify_spec.rb +++ b/spec/lib/interactify_spec.rb @@ -7,7 +7,7 @@ describe ".validate_app" do before do - wiring = instance_double(Interactify::Wiring, validate_app: 'ok') + wiring = instance_double(Interactify::Wiring, validate_app: "ok") expect(Interactify::Wiring) .to receive(:new)