Skip to content

Commit

Permalink
Merge branch 'support-if-then-syntax' into namespace
Browse files Browse the repository at this point in the history
* support-if-then-syntax:
  extract IfKlass
  add support for then else in if method syntax
  fix constant name
  rename to success_arg etc
  add missing specs
  • Loading branch information
markburns committed Dec 29, 2023
2 parents 8d981e9 + 398f8bc commit 80ba64f
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 36 deletions.
2 changes: 1 addition & 1 deletion lib/interactify.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 9 additions & 3 deletions lib/interactify/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,18 @@ def each(plural_resource_name, *each_loop_klasses)
)
end

def if(condition, succcess_interactor, failure_interactor = nil)
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

IfInteractor.attach_klass(
self,
condition,
succcess_interactor,
failure_interactor
then_else[:then],
then_else[:else]
)
end

Expand Down
2 changes: 1 addition & 1 deletion lib/interactify/dsl/each_chain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
71 changes: 40 additions & 31 deletions lib/interactify/dsl/if_interactor.rb
Original file line number Diff line number Diff line change
@@ -1,55 +1,47 @@
# frozen_string_literal: true

require "interactify/dsl/unique_klass_name"
require "interactify/dsl/if_klass"

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
# that iterates over the packages and
# uses the passed in each_loop_klasses
# rubocop:disable all
def klass
this = self

Class.new do
include Interactor
include Interactor::Contracts

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
IfKlass.new(self).klass
end

define_method(:inspect) do
"<#{this.namespace}::#{this.if_klass_name} #{this.condition} ? #{this.success_interactor} : #{this.failure_interactor}>"
end
# 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
# rubocop:enable all

def attach_klass
name = if_klass_name
Expand All @@ -62,10 +54,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

private

UniqueKlassName.for(namespace, prefix)
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
Expand Down
80 changes: 80 additions & 0 deletions lib/interactify/dsl/if_klass.rb
Original file line number Diff line number Diff line change
@@ -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
80 changes: 80 additions & 0 deletions spec/lib/interactify/dsl_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# 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) do
lambda { |ctx|
ctx.success1 = true
}
end
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
35 changes: 35 additions & 0 deletions spec/lib/interactify_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 80ba64f

Please sign in to comment.