interactify 0.3.0.pre.alpha.1 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/.ruby-version +1 -0
- data/Appraisals +23 -0
- data/CHANGELOG.md +17 -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 +96 -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 +49 -72
- 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,96 @@
|
|
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
|
+
self.class.klasses.each do |interactor| # [A, B, C].each do |interactor|
|
45
|
+
interactor.call!(context) # interactor.call!(context)
|
46
|
+
end # end
|
47
|
+
end # end
|
48
|
+
|
49
|
+
context[this.singular_resource_name] = nil # context.package = nil
|
50
|
+
context[this.singular_resource_index_name] = nil # context.package_index = nil
|
51
|
+
|
52
|
+
context # context
|
53
|
+
end # end
|
54
|
+
|
55
|
+
define_singleton_method(:klasses) do # def self.klasses
|
56
|
+
klasses = instance_variable_get(:@klasses) # @klasses ||= Wrapper.wrap_many(self, [A, B, C])
|
57
|
+
return klasses if klasses
|
58
|
+
|
59
|
+
instance_variable_set(:@klasses, Wrapper.wrap_many(self, this.each_loop_klasses))
|
60
|
+
end
|
61
|
+
|
62
|
+
# "<SomeNamespace::EachPackage iterates_over: [A, B, C]>"
|
63
|
+
define_method(:inspect) do
|
64
|
+
"<#{this.namespace}::#{this.iterator_klass_name} iterates_over: #{this.each_loop_klasses.inspect}>"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
# rubocop:enable all
|
69
|
+
|
70
|
+
def attach_klass
|
71
|
+
name = iterator_klass_name
|
72
|
+
|
73
|
+
namespace.const_set(name, klass)
|
74
|
+
namespace.const_get(name)
|
75
|
+
end
|
76
|
+
|
77
|
+
def namespace
|
78
|
+
evaluating_receiver
|
79
|
+
end
|
80
|
+
|
81
|
+
def iterator_klass_name
|
82
|
+
prefix = "Each#{singular_resource_name.to_s.camelize}"
|
83
|
+
|
84
|
+
UniqueKlassName.for(namespace, prefix)
|
85
|
+
end
|
86
|
+
|
87
|
+
def singular_resource_name
|
88
|
+
plural_resource_name.to_s.singularize.to_sym
|
89
|
+
end
|
90
|
+
|
91
|
+
def singular_resource_index_name
|
92
|
+
"#{singular_resource_name}_index".to_sym
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
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
|