interactify 0.3.0.pre.alpha.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/.ruby-version +1 -0
- data/Appraisals +23 -0
- data/CHANGELOG.md +14 -0
- data/README.md +33 -44
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/no_railties_no_sidekiq.gemfile +18 -0
- data/gemfiles/no_railties_no_sidekiq.gemfile.lock +127 -0
- data/gemfiles/railties_6.gemfile +14 -0
- data/gemfiles/railties_6.gemfile.lock +253 -0
- data/gemfiles/railties_6_no_sidekiq.gemfile +19 -0
- data/gemfiles/railties_6_no_sidekiq.gemfile.lock +159 -0
- data/gemfiles/railties_6_sidekiq.gemfile +20 -0
- data/gemfiles/railties_6_sidekiq.gemfile.lock +168 -0
- data/gemfiles/railties_7_no_sidekiq.gemfile +19 -0
- data/gemfiles/railties_7_no_sidekiq.gemfile.lock +158 -0
- data/gemfiles/railties_7_sidekiq.gemfile +20 -0
- data/gemfiles/railties_7_sidekiq.gemfile.lock +167 -0
- data/lib/interactify/async/job_klass.rb +63 -0
- data/lib/interactify/async/job_maker.rb +58 -0
- data/lib/interactify/async/jobable.rb +96 -0
- data/lib/interactify/async/null_job.rb +23 -0
- data/lib/interactify/configuration.rb +15 -0
- data/lib/interactify/contracts/call_wrapper.rb +19 -0
- data/lib/interactify/contracts/failure.rb +8 -0
- data/lib/interactify/contracts/helpers.rb +81 -0
- data/lib/interactify/contracts/mismatching_promise_error.rb +19 -0
- data/lib/interactify/contracts/promising.rb +36 -0
- data/lib/interactify/contracts/setup.rb +39 -0
- data/lib/interactify/dsl/each_chain.rb +90 -0
- data/lib/interactify/dsl/if_interactor.rb +81 -0
- data/lib/interactify/dsl/if_klass.rb +82 -0
- data/lib/interactify/dsl/organizer.rb +32 -0
- data/lib/interactify/dsl/unique_klass_name.rb +23 -0
- data/lib/interactify/dsl/wrapper.rb +74 -0
- data/lib/interactify/dsl.rb +12 -6
- data/lib/interactify/rspec_matchers/matchers.rb +68 -0
- data/lib/interactify/version.rb +1 -1
- data/lib/interactify/{interactor_wiring → wiring}/callable_representation.rb +2 -2
- data/lib/interactify/{interactor_wiring → wiring}/constants.rb +4 -4
- data/lib/interactify/{interactor_wiring → wiring}/error_context.rb +1 -1
- data/lib/interactify/{interactor_wiring → wiring}/files.rb +1 -1
- data/lib/interactify/{interactor_wiring.rb → wiring.rb} +5 -5
- data/lib/interactify.rb +58 -38
- metadata +48 -71
- data/lib/interactify/async_job_klass.rb +0 -61
- data/lib/interactify/call_wrapper.rb +0 -17
- data/lib/interactify/contract_failure.rb +0 -6
- data/lib/interactify/contract_helpers.rb +0 -71
- data/lib/interactify/each_chain.rb +0 -88
- data/lib/interactify/if_interactor.rb +0 -70
- data/lib/interactify/interactor_wrapper.rb +0 -72
- data/lib/interactify/job_maker.rb +0 -56
- data/lib/interactify/jobable.rb +0 -92
- data/lib/interactify/mismatching_promise_error.rb +0 -17
- data/lib/interactify/organizer.rb +0 -30
- data/lib/interactify/promising.rb +0 -34
- data/lib/interactify/rspec/matchers.rb +0 -67
- data/lib/interactify/unique_klass_name.rb +0 -21
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Interactify
|
4
|
+
module Async
|
5
|
+
class JobKlass
|
6
|
+
attr_reader :container_klass, :klass_suffix
|
7
|
+
|
8
|
+
def initialize(container_klass:, klass_suffix:)
|
9
|
+
@container_klass = container_klass
|
10
|
+
@klass_suffix = klass_suffix
|
11
|
+
end
|
12
|
+
|
13
|
+
def async_job_klass
|
14
|
+
klass = Class.new do
|
15
|
+
include Interactor
|
16
|
+
include Interactor::Contracts
|
17
|
+
end
|
18
|
+
|
19
|
+
attach_call(klass)
|
20
|
+
attach_call!(klass)
|
21
|
+
|
22
|
+
klass
|
23
|
+
end
|
24
|
+
|
25
|
+
def attach_call(async_job_klass)
|
26
|
+
# e.g. SomeInteractor::AsyncWithSuffix.call(foo: 'bar')
|
27
|
+
async_job_klass.send(:define_singleton_method, :call) do |context|
|
28
|
+
call!(context)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def attach_call!(async_job_klass)
|
33
|
+
this = self
|
34
|
+
|
35
|
+
# e.g. SomeInteractor::AsyncWithSuffix.call!(foo: 'bar')
|
36
|
+
async_job_klass.send(:define_singleton_method, :call!) do |context|
|
37
|
+
# e.g. SomeInteractor::JobWithSuffix
|
38
|
+
job_klass = this.container_klass.const_get("Job#{this.klass_suffix}")
|
39
|
+
|
40
|
+
# e.g. SomeInteractor::JobWithSuffix.perform_async({foo: 'bar'})
|
41
|
+
job_klass.perform_async(this.args(context))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def args(context)
|
46
|
+
args = context.to_h.stringify_keys
|
47
|
+
|
48
|
+
return args unless container_klass.respond_to?(:expected_keys)
|
49
|
+
|
50
|
+
restrict_to_optional_or_keys_from_contract(args)
|
51
|
+
end
|
52
|
+
|
53
|
+
def restrict_to_optional_or_keys_from_contract(args)
|
54
|
+
keys = container_klass.expected_keys.map(&:to_s)
|
55
|
+
|
56
|
+
optional = Array(container_klass.optional_attrs).map(&:to_s)
|
57
|
+
keys += optional
|
58
|
+
|
59
|
+
args.slice(*keys)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "interactify/async/job_klass"
|
4
|
+
require "interactify/async/null_job"
|
5
|
+
|
6
|
+
module Interactify
|
7
|
+
module Async
|
8
|
+
class JobMaker
|
9
|
+
attr_reader :opts, :method_name, :container_klass, :klass_suffix
|
10
|
+
|
11
|
+
def initialize(container_klass:, opts:, klass_suffix:, method_name: :call!)
|
12
|
+
@container_klass = container_klass
|
13
|
+
@opts = opts
|
14
|
+
@method_name = method_name
|
15
|
+
@klass_suffix = klass_suffix
|
16
|
+
end
|
17
|
+
|
18
|
+
concerning :JobClass do
|
19
|
+
def job_klass
|
20
|
+
@job_klass ||= define_job_klass
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def define_job_klass
|
26
|
+
return NullJob if Interactify.sidekiq_missing?
|
27
|
+
|
28
|
+
this = self
|
29
|
+
|
30
|
+
invalid_keys = this.opts.symbolize_keys.keys - %i[queue retry dead backtrace pool tags]
|
31
|
+
|
32
|
+
raise ArgumentError, "Invalid keys: #{invalid_keys}" if invalid_keys.any?
|
33
|
+
|
34
|
+
build_job_klass(opts).tap do |klass|
|
35
|
+
klass.const_set(:JOBABLE_OPTS, opts)
|
36
|
+
klass.const_set(:JOBABLE_METHOD_NAME, method_name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def build_job_klass(opts)
|
41
|
+
Class.new do
|
42
|
+
include Sidekiq::Job
|
43
|
+
|
44
|
+
sidekiq_options(opts)
|
45
|
+
|
46
|
+
def perform(...)
|
47
|
+
self.class.module_parent.send(self.class::JOBABLE_METHOD_NAME, ...)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def async_job_klass
|
54
|
+
JobKlass.new(container_klass:, klass_suffix:).async_job_klass
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "interactify/async/job_maker"
|
4
|
+
|
5
|
+
module Interactify
|
6
|
+
module Async
|
7
|
+
module Jobable
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
# e.g. if Klass < Base
|
11
|
+
# and Base has a Base::Job class
|
12
|
+
#
|
13
|
+
# then let's make sure to define Klass::Job separately
|
14
|
+
included do |base|
|
15
|
+
next if Interactify.sidekiq_missing?
|
16
|
+
|
17
|
+
def base.inherited(klass)
|
18
|
+
super_klass = klass.superclass
|
19
|
+
super_job = super_klass::Job # really spiffing
|
20
|
+
|
21
|
+
opts = super_job::JOBABLE_OPTS
|
22
|
+
jobable_method_name = super_job::JOBABLE_METHOD_NAME
|
23
|
+
|
24
|
+
to_call = defined?(super_klass::Async) ? :interactor_job : :job_calling
|
25
|
+
|
26
|
+
klass.send(to_call, opts:, method_name: jobable_method_name)
|
27
|
+
super(klass)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class_methods do
|
32
|
+
# create a Job class and an Async class
|
33
|
+
# see job_calling for details on the Job class
|
34
|
+
#
|
35
|
+
# the Async class is a wrapper around the Job class
|
36
|
+
# that allows it to be used in an interactor chain
|
37
|
+
#
|
38
|
+
# E.g.
|
39
|
+
#
|
40
|
+
# class ExampleInteractor
|
41
|
+
# include Interactify
|
42
|
+
# expect :foo
|
43
|
+
#
|
44
|
+
# include Jobable
|
45
|
+
# interactor_job
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# doing the following will immediately enqueue a job
|
49
|
+
# that calls the interactor ExampleInteractor with (foo: 'bar')
|
50
|
+
# ExampleInteractor::Async.call(foo: 'bar')
|
51
|
+
#
|
52
|
+
# it will also ensure to pluck only the expects from the context
|
53
|
+
# so that you can have other non primitive values in the context
|
54
|
+
# but the job will only have the expects passed to it
|
55
|
+
#
|
56
|
+
# obviously you will need to be aware that later interactors
|
57
|
+
# in an interactor chain cannot depend on the result of the async
|
58
|
+
# interactor
|
59
|
+
def interactor_job(method_name: :call!, opts: {}, klass_suffix: "")
|
60
|
+
job_maker = JobMaker.new(container_klass: self, opts:, method_name:, klass_suffix:)
|
61
|
+
# with WhateverInteractor::Job you can perform the interactor as a job
|
62
|
+
# from sidekiq
|
63
|
+
# e.g. WhateverInteractor::Job.perform_async(...)
|
64
|
+
const_set("Job#{klass_suffix}", job_maker.job_klass)
|
65
|
+
|
66
|
+
# with WhateverInteractor::Async you can call WhateverInteractor::Job
|
67
|
+
# in an organizer oro on its oen using normal interactor call call! semantics
|
68
|
+
# e.g. WhateverInteractor::Async.call(...)
|
69
|
+
# WhateverInteractor::Async.call!(...)
|
70
|
+
const_set("Async#{klass_suffix}", job_maker.async_job_klass)
|
71
|
+
end
|
72
|
+
|
73
|
+
# if this was defined in ExampleClass this creates the following class
|
74
|
+
# ExampleClass::Job
|
75
|
+
# this class ia added as a convenience so you can easily turn a
|
76
|
+
# class method into a job
|
77
|
+
#
|
78
|
+
# Example:
|
79
|
+
#
|
80
|
+
# class ExampleClass
|
81
|
+
# include Jobable
|
82
|
+
# job_calling method_name: :some_method
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# # the following class is created that you can use to enqueue a job
|
86
|
+
# in the sidekiq yaml file
|
87
|
+
# ExampleClass::Job.some_method
|
88
|
+
def job_calling(method_name:, opts: {}, klass_suffix: "")
|
89
|
+
job_maker = JobMaker.new(container_klass: self, opts:, method_name:, klass_suffix:)
|
90
|
+
|
91
|
+
const_set("Job#{klass_suffix}", job_maker.job_klass)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Interactify
|
4
|
+
module Async
|
5
|
+
class NullJob
|
6
|
+
def method_missing(...)
|
7
|
+
self
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.method_missing(...)
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def respond_to_missing?(...)
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.respond_to_missing?(...)
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Interactify
|
4
|
+
module Contracts
|
5
|
+
module CallWrapper
|
6
|
+
# https://github.com/collectiveidea/interactor/blob/57b2af9a5a5afeb2c01059c40b792485cc21b052/lib/interactor.rb#L114
|
7
|
+
# Interactor#run calls Interactor#run!
|
8
|
+
# https://github.com/collectiveidea/interactor/blob/57b2af9a5a5afeb2c01059c40b792485cc21b052/lib/interactor.rb#L49
|
9
|
+
# Interactor.call calls Interactor.run
|
10
|
+
#
|
11
|
+
# The non bang methods call the bang methods and rescue
|
12
|
+
def run
|
13
|
+
@_interactor_called_by_non_bang_method = true
|
14
|
+
|
15
|
+
super
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "interactify/async/jobable"
|
4
|
+
require "interactify/contracts/call_wrapper"
|
5
|
+
require "interactify/contracts/failure"
|
6
|
+
require "interactify/contracts/setup"
|
7
|
+
require "interactify/dsl/organizer"
|
8
|
+
|
9
|
+
module Interactify
|
10
|
+
module Contracts
|
11
|
+
module Helpers
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
class_methods do
|
15
|
+
def expect(*attrs, filled: true)
|
16
|
+
Setup.expects(context: self, attrs:, filled:)
|
17
|
+
end
|
18
|
+
|
19
|
+
def promise(*attrs, filled: true, should_delegate: true)
|
20
|
+
Setup.promises(context: self, attrs:, filled:, should_delegate:)
|
21
|
+
end
|
22
|
+
|
23
|
+
def promising(*args)
|
24
|
+
Promising.validate(self, *args)
|
25
|
+
end
|
26
|
+
|
27
|
+
def promised_keys
|
28
|
+
_interactify_extract_keys(contract.promises)
|
29
|
+
end
|
30
|
+
|
31
|
+
def expected_keys
|
32
|
+
_interactify_extract_keys(contract.expectations)
|
33
|
+
end
|
34
|
+
|
35
|
+
def optional(*attrs)
|
36
|
+
@optional_attrs ||= []
|
37
|
+
@optional_attrs += attrs
|
38
|
+
|
39
|
+
delegate(*attrs, to: :context)
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :optional_attrs
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# this is the most brittle part of the code, relying on
|
47
|
+
# interactor-contracts internals
|
48
|
+
# so extracted it to here so change is isolated
|
49
|
+
def _interactify_extract_keys(clauses)
|
50
|
+
clauses.instance_eval { @terms }.json&.rules&.keys
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
included do
|
55
|
+
c = Class.new(Contracts::Failure)
|
56
|
+
# example self is Whatever::SomeInteractor
|
57
|
+
# failure class: Whatever::SomeInteractor::InteractorContractFailure
|
58
|
+
const_set "InteractorContractFailure", c
|
59
|
+
prepend Contracts::CallWrapper
|
60
|
+
include Dsl::Organizer
|
61
|
+
|
62
|
+
on_breach do |breaches|
|
63
|
+
breaches = breaches.map { |b| { b.property => b.messages } }.inject(&:merge)
|
64
|
+
|
65
|
+
Interactify.trigger_contract_breach_hook(context, breaches)
|
66
|
+
|
67
|
+
if @_interactor_called_by_non_bang_method == true
|
68
|
+
context.fail! contract_failures: breaches
|
69
|
+
else
|
70
|
+
# e.g. raises
|
71
|
+
# SomeNamespace::SomeClass::ContractFailure, {whatever: 'is missing'}
|
72
|
+
# but also sending the context into Sentry
|
73
|
+
exception = c.new(breaches.to_json)
|
74
|
+
Interactify.trigger_before_raise_hook(exception)
|
75
|
+
raise exception
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "interactify/contracts/failure"
|
4
|
+
|
5
|
+
module Interactify
|
6
|
+
module Contracts
|
7
|
+
class MismatchingPromiseError < Contracts::Failure
|
8
|
+
def initialize(interactor, promising, promised_keys)
|
9
|
+
super <<~MESSAGE.chomp
|
10
|
+
#{interactor} does not promise:
|
11
|
+
#{promising.inspect}
|
12
|
+
|
13
|
+
Actual promises are:
|
14
|
+
#{promised_keys.inspect}
|
15
|
+
MESSAGE
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "interactify/contracts/mismatching_promise_error"
|
4
|
+
|
5
|
+
module Interactify
|
6
|
+
module Contracts
|
7
|
+
class Promising
|
8
|
+
attr_reader :interactor, :promising
|
9
|
+
|
10
|
+
def self.validate(interactor, *promising)
|
11
|
+
new(interactor, *promising).validate
|
12
|
+
|
13
|
+
interactor
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(interactor, *promising)
|
17
|
+
@interactor = interactor
|
18
|
+
@promising = format_keys promising
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate
|
22
|
+
return if promising == promised_keys
|
23
|
+
|
24
|
+
raise MismatchingPromiseError.new(interactor, promising, promised_keys)
|
25
|
+
end
|
26
|
+
|
27
|
+
def promised_keys
|
28
|
+
format_keys interactor.promised_keys
|
29
|
+
end
|
30
|
+
|
31
|
+
def format_keys(keys)
|
32
|
+
Array(keys).compact.map(&:to_sym).sort
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Interactify
|
4
|
+
module Contracts
|
5
|
+
class Setup
|
6
|
+
def self.expects(context:, attrs:, filled:)
|
7
|
+
new(context:, attrs:, filled:).setup(:expects)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.promises(context:, attrs:, filled:, should_delegate:)
|
11
|
+
new(context:, attrs:, filled:, should_delegate:).setup(:promises)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(context:, attrs:, filled:, should_delegate: true)
|
15
|
+
@context = context
|
16
|
+
@attrs = attrs
|
17
|
+
@filled = filled
|
18
|
+
@should_delegate = should_delegate
|
19
|
+
end
|
20
|
+
|
21
|
+
def setup(meth)
|
22
|
+
this = self
|
23
|
+
|
24
|
+
@context.send(meth) do
|
25
|
+
this.setup_attrs self
|
26
|
+
end
|
27
|
+
|
28
|
+
@context.delegate(*@attrs, to: :context) if @should_delegate
|
29
|
+
end
|
30
|
+
|
31
|
+
def setup_attrs(contract)
|
32
|
+
@attrs.each do |attr|
|
33
|
+
field = contract.required(attr)
|
34
|
+
field.filled if @filled
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "interactify/dsl/unique_klass_name"
|
4
|
+
|
5
|
+
module Interactify
|
6
|
+
module Dsl
|
7
|
+
class EachChain
|
8
|
+
attr_reader :each_loop_klasses, :plural_resource_name, :evaluating_receiver
|
9
|
+
|
10
|
+
def self.attach_klass(evaluating_receiver, plural_resource_name, *each_loop_klasses)
|
11
|
+
iteratable = new(each_loop_klasses, plural_resource_name, evaluating_receiver)
|
12
|
+
iteratable.attach_klass
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(each_loop_klasses, plural_resource_name, evaluating_receiver)
|
16
|
+
@each_loop_klasses = each_loop_klasses
|
17
|
+
@plural_resource_name = plural_resource_name
|
18
|
+
@evaluating_receiver = evaluating_receiver
|
19
|
+
end
|
20
|
+
|
21
|
+
# allows us to dynamically create an interactor chain
|
22
|
+
# that iterates over the packages and
|
23
|
+
# uses the passed in each_loop_klasses
|
24
|
+
# rubocop:disable all
|
25
|
+
def klass
|
26
|
+
this = self
|
27
|
+
|
28
|
+
Class.new do # class SomeNamespace::EachPackage
|
29
|
+
include Interactify # include Interactify
|
30
|
+
|
31
|
+
expects do # expects do
|
32
|
+
required(this.plural_resource_name) # required(:packages)
|
33
|
+
end # end
|
34
|
+
|
35
|
+
define_singleton_method(:source_location) do # def self.source_location
|
36
|
+
const_source_location this.evaluating_receiver.to_s # [file, line]
|
37
|
+
end # end
|
38
|
+
|
39
|
+
define_method(:run!) do # def run!
|
40
|
+
context.send(this.plural_resource_name).each_with_index do |resource, index|# context.packages.each_with_index do |package, index|
|
41
|
+
context[this.singular_resource_name] = resource # context.package = package
|
42
|
+
context[this.singular_resource_index_name] = index # context.package_index = index
|
43
|
+
|
44
|
+
klasses = Wrapper.wrap_many(self, this.each_loop_klasses)
|
45
|
+
|
46
|
+
klasses.each do |interactor| # [A, B, C].each do |interactor|
|
47
|
+
interactor.call!(context) # interactor.call!(context)
|
48
|
+
end # end
|
49
|
+
end # end
|
50
|
+
|
51
|
+
context[this.singular_resource_name] = nil # context.package = nil
|
52
|
+
context[this.singular_resource_index_name] = nil # context.package_index = nil
|
53
|
+
|
54
|
+
context # context
|
55
|
+
end # end
|
56
|
+
|
57
|
+
define_method(:inspect) do
|
58
|
+
"<#{this.namespace}::#{this.iterator_klass_name} iterates_over: #{this.each_loop_klasses.inspect}>"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
# rubocop:enable all
|
63
|
+
|
64
|
+
def attach_klass
|
65
|
+
name = iterator_klass_name
|
66
|
+
|
67
|
+
namespace.const_set(name, klass)
|
68
|
+
namespace.const_get(name)
|
69
|
+
end
|
70
|
+
|
71
|
+
def namespace
|
72
|
+
evaluating_receiver
|
73
|
+
end
|
74
|
+
|
75
|
+
def iterator_klass_name
|
76
|
+
prefix = "Each#{singular_resource_name.to_s.camelize}"
|
77
|
+
|
78
|
+
UniqueKlassName.for(namespace, prefix)
|
79
|
+
end
|
80
|
+
|
81
|
+
def singular_resource_name
|
82
|
+
plural_resource_name.to_s.singularize.to_sym
|
83
|
+
end
|
84
|
+
|
85
|
+
def singular_resource_index_name
|
86
|
+
"#{singular_resource_name}_index".to_sym
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "interactify/dsl/unique_klass_name"
|
4
|
+
require "interactify/dsl/if_klass"
|
5
|
+
|
6
|
+
module Interactify
|
7
|
+
module Dsl
|
8
|
+
class IfInteractor
|
9
|
+
attr_reader :condition, :evaluating_receiver
|
10
|
+
|
11
|
+
def self.attach_klass(evaluating_receiver, condition, succcess_interactor, failure_interactor)
|
12
|
+
ifable = new(evaluating_receiver, condition, succcess_interactor, failure_interactor)
|
13
|
+
ifable.attach_klass
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(evaluating_receiver, condition, succcess_arg, failure_arg)
|
17
|
+
@evaluating_receiver = evaluating_receiver
|
18
|
+
@condition = condition
|
19
|
+
@success_arg = succcess_arg
|
20
|
+
@failure_arg = failure_arg
|
21
|
+
end
|
22
|
+
|
23
|
+
def success_interactor
|
24
|
+
@success_interactor ||= build_chain(@success_arg, true)
|
25
|
+
end
|
26
|
+
|
27
|
+
def failure_interactor
|
28
|
+
@failure_interactor ||= build_chain(@failure_arg, false)
|
29
|
+
end
|
30
|
+
|
31
|
+
# allows us to dynamically create an interactor chain
|
32
|
+
# that iterates over the packages and
|
33
|
+
# uses the passed in each_loop_klasses
|
34
|
+
def klass
|
35
|
+
IfKlass.new(self).klass
|
36
|
+
end
|
37
|
+
|
38
|
+
# so we have something to attach subclasses to during building
|
39
|
+
# of the outer class, before we finalize the outer If class
|
40
|
+
def klass_basis
|
41
|
+
@klass_basis ||= Class.new do
|
42
|
+
include Interactify
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def attach_klass
|
47
|
+
name = if_klass_name
|
48
|
+
namespace.const_set(name, klass)
|
49
|
+
namespace.const_get(name)
|
50
|
+
end
|
51
|
+
|
52
|
+
def namespace
|
53
|
+
evaluating_receiver
|
54
|
+
end
|
55
|
+
|
56
|
+
def if_klass_name
|
57
|
+
@if_klass_name ||=
|
58
|
+
begin
|
59
|
+
prefix = condition.is_a?(Proc) ? "Proc" : condition
|
60
|
+
prefix = "If#{prefix.to_s.camelize}"
|
61
|
+
|
62
|
+
UniqueKlassName.for(namespace, prefix)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def build_chain(arg, truthiness)
|
69
|
+
return if arg.nil?
|
70
|
+
|
71
|
+
case arg
|
72
|
+
when Array
|
73
|
+
name = "If#{condition.to_s.camelize}#{truthiness ? 'IsTruthy' : 'IsFalsey'}"
|
74
|
+
klass_basis.chain(name, *arg)
|
75
|
+
else
|
76
|
+
arg
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|