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,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Interactify
|
4
|
+
module Dsl
|
5
|
+
class IfKlass
|
6
|
+
attr_reader :if_builder
|
7
|
+
|
8
|
+
def initialize(if_builder)
|
9
|
+
@if_builder = if_builder
|
10
|
+
end
|
11
|
+
|
12
|
+
def klass
|
13
|
+
attach_expectations
|
14
|
+
attach_source_location
|
15
|
+
attach_run!
|
16
|
+
attach_inspect
|
17
|
+
|
18
|
+
if_builder.klass_basis
|
19
|
+
end
|
20
|
+
|
21
|
+
def run!(context)
|
22
|
+
result = condition.is_a?(Proc) ? condition.call(context) : context.send(condition)
|
23
|
+
|
24
|
+
interactor = result ? success_interactor : failure_interactor
|
25
|
+
interactor.respond_to?(:call!) ? interactor.call!(context) : interactor&.call(context)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def attach_source_location
|
31
|
+
attach do |_klass, this|
|
32
|
+
define_singleton_method(:source_location) do # def self.source_location
|
33
|
+
const_source_location this.evaluating_receiver.to_s # [file, line]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def attach_expectations
|
39
|
+
attach do |klass, this|
|
40
|
+
klass.expects do
|
41
|
+
required(this.condition) unless this.condition.is_a?(Proc)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def attach_run!
|
47
|
+
this = self
|
48
|
+
|
49
|
+
attach_method(:run!) do
|
50
|
+
this.run!(context)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
delegate :condition, :success_interactor, :failure_interactor, to: :if_builder
|
55
|
+
|
56
|
+
def attach_inspect
|
57
|
+
this = if_builder
|
58
|
+
|
59
|
+
attach_method(:inspect) do
|
60
|
+
name = "#{this.namespace}::#{this.if_klass_name}"
|
61
|
+
"<#{name} #{this.condition} ? #{this.success_interactor} : #{this.failure_interactor}>"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# rubocop: disable Naming/BlockForwarding
|
66
|
+
def attach_method(name, &block)
|
67
|
+
attach do |klass, _this|
|
68
|
+
klass.define_method(name, &block)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
# rubocop: enable Naming/BlockForwarding
|
72
|
+
|
73
|
+
def attach
|
74
|
+
this = if_builder
|
75
|
+
|
76
|
+
this.klass_basis.instance_eval do
|
77
|
+
yield self, this
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "interactify/dsl/wrapper"
|
4
|
+
|
5
|
+
module Interactify
|
6
|
+
module Dsl
|
7
|
+
module Organizer
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
def organize(*interactors)
|
12
|
+
wrapped = Wrapper.wrap_many(self, interactors)
|
13
|
+
|
14
|
+
super(*wrapped)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def call
|
19
|
+
self.class.organized.each do |interactor|
|
20
|
+
instance = interactor.new(context)
|
21
|
+
|
22
|
+
instance.instance_variable_set(
|
23
|
+
:@_interactor_called_by_non_bang_method,
|
24
|
+
@_interactor_called_by_non_bang_method
|
25
|
+
)
|
26
|
+
|
27
|
+
instance.tap(&:run!)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Interactify
|
4
|
+
module Dsl
|
5
|
+
module UniqueKlassName
|
6
|
+
def self.for(namespace, prefix)
|
7
|
+
id = generate_unique_id
|
8
|
+
klass_name = :"#{prefix}#{id}"
|
9
|
+
|
10
|
+
while namespace.const_defined?(klass_name)
|
11
|
+
id = generate_unique_id
|
12
|
+
klass_name = :"#{prefix}#{id}"
|
13
|
+
end
|
14
|
+
|
15
|
+
klass_name.to_sym
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.generate_unique_id
|
19
|
+
rand(10_000)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "interactify/dsl/unique_klass_name"
|
4
|
+
|
5
|
+
module Interactify
|
6
|
+
module Dsl
|
7
|
+
class Wrapper
|
8
|
+
attr_reader :organizer, :interactor
|
9
|
+
|
10
|
+
def self.wrap_many(organizer, interactors)
|
11
|
+
Array(interactors).map do |interactor|
|
12
|
+
wrap(organizer, interactor)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.wrap(organizer, interactor)
|
17
|
+
new(organizer, interactor).wrap
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(organizer, interactor)
|
21
|
+
@organizer = organizer
|
22
|
+
@interactor = interactor
|
23
|
+
end
|
24
|
+
|
25
|
+
def wrap
|
26
|
+
case interactor
|
27
|
+
when Hash
|
28
|
+
wrap_conditional
|
29
|
+
when Array
|
30
|
+
wrap_chain
|
31
|
+
when Proc
|
32
|
+
wrap_proc
|
33
|
+
else
|
34
|
+
interactor
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def wrap_chain
|
39
|
+
return self.class.wrap(organizer, interactor.first) if interactor.length == 1
|
40
|
+
|
41
|
+
klass_name = UniqueKlassName.for(organizer, "Chained")
|
42
|
+
organizer.chain(klass_name, *interactor.map { self.class.wrap(organizer, _1) })
|
43
|
+
end
|
44
|
+
|
45
|
+
def wrap_conditional
|
46
|
+
raise ArgumentError, "Hash must have at least :if, and :then key" unless condition && then_do
|
47
|
+
|
48
|
+
return organizer.if(condition, then_do, else_do) if else_do
|
49
|
+
|
50
|
+
organizer.if(condition, then_do)
|
51
|
+
end
|
52
|
+
|
53
|
+
def condition = interactor[:if]
|
54
|
+
def then_do = interactor[:then]
|
55
|
+
def else_do = interactor[:else]
|
56
|
+
|
57
|
+
def wrap_proc
|
58
|
+
this = self
|
59
|
+
|
60
|
+
Class.new do
|
61
|
+
include Interactify
|
62
|
+
|
63
|
+
define_singleton_method :wrapped do
|
64
|
+
this.interactor
|
65
|
+
end
|
66
|
+
|
67
|
+
define_method(:call) do
|
68
|
+
this.interactor.call(context)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/interactify/dsl.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "interactify/each_chain"
|
4
|
-
require "interactify/if_interactor"
|
5
|
-
require "interactify/unique_klass_name"
|
3
|
+
require "interactify/dsl/each_chain"
|
4
|
+
require "interactify/dsl/if_interactor"
|
5
|
+
require "interactify/dsl/unique_klass_name"
|
6
6
|
|
7
7
|
module Interactify
|
8
8
|
module Dsl
|
@@ -22,12 +22,18 @@ module Interactify
|
|
22
22
|
)
|
23
23
|
end
|
24
24
|
|
25
|
-
def if(condition,
|
25
|
+
def if(condition, success_arg, failure_arg = nil)
|
26
|
+
then_else = if success_arg.is_a?(Hash) && failure_arg.nil?
|
27
|
+
success_arg.slice(:then, :else)
|
28
|
+
else
|
29
|
+
{ then: success_arg, else: failure_arg }
|
30
|
+
end
|
31
|
+
|
26
32
|
IfInteractor.attach_klass(
|
27
33
|
self,
|
28
34
|
condition,
|
29
|
-
|
30
|
-
|
35
|
+
then_else[:then],
|
36
|
+
then_else[:else]
|
31
37
|
)
|
32
38
|
end
|
33
39
|
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "interactify/wiring"
|
4
|
+
|
5
|
+
module Interactify
|
6
|
+
module RSpecMatchers
|
7
|
+
class ContractMatcher
|
8
|
+
attr_reader :actual, :expected_values, :actual_values, :type
|
9
|
+
|
10
|
+
def initialize(actual, expected_values, actual_values, type)
|
11
|
+
@actual = actual
|
12
|
+
@expected_values = expected_values
|
13
|
+
@actual_values = actual_values
|
14
|
+
@type = type
|
15
|
+
end
|
16
|
+
|
17
|
+
def failure_message
|
18
|
+
message = "expected #{actual} to #{type} #{expected_values.inspect}"
|
19
|
+
message += "\n\tmissing: #{missing}" if missing.any?
|
20
|
+
message += "\n\textra: #{extra}" if extra.any?
|
21
|
+
message
|
22
|
+
end
|
23
|
+
|
24
|
+
def valid?
|
25
|
+
missing.empty? && extra.empty?
|
26
|
+
end
|
27
|
+
|
28
|
+
def missing
|
29
|
+
expected_values - actual_values
|
30
|
+
end
|
31
|
+
|
32
|
+
def extra
|
33
|
+
actual_values - expected_values
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Custom matchers that implement expect_inputs, promise_outputs, organize_interactors
|
40
|
+
# e.g. expect(described_class).to expect_inputs(:connection, :order)
|
41
|
+
# e.g. expect(described_class).to promise_outputs(:request_logger)
|
42
|
+
# e.g. expect(described_class).to organize_interactors(SeparateIntoPackages, SendPackagesToSeko)
|
43
|
+
[
|
44
|
+
%i[expect_inputs expected_keys],
|
45
|
+
%i[promise_outputs promised_keys],
|
46
|
+
%i[organize_interactors organized]
|
47
|
+
].each do |type, meth|
|
48
|
+
RSpec::Matchers.define type do |*expected_values|
|
49
|
+
match do |actual|
|
50
|
+
next false unless actual.respond_to?(meth)
|
51
|
+
|
52
|
+
actual_values = Array(actual.send(meth))
|
53
|
+
|
54
|
+
@contract_matcher = Interactify::RSpecMatchers::ContractMatcher.new(
|
55
|
+
actual,
|
56
|
+
expected_values,
|
57
|
+
actual_values,
|
58
|
+
type.to_s.gsub("_", " ")
|
59
|
+
)
|
60
|
+
|
61
|
+
@contract_matcher.valid?
|
62
|
+
end
|
63
|
+
|
64
|
+
failure_message do
|
65
|
+
@contract_matcher.failure_message
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/interactify/version.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "interactify/
|
3
|
+
require "interactify/wiring/error_context"
|
4
4
|
|
5
5
|
module Interactify
|
6
|
-
class
|
6
|
+
class Wiring
|
7
7
|
class CallableRepresentation
|
8
8
|
attr_reader :filename, :klass, :wiring
|
9
9
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Interactify
|
4
|
-
class
|
4
|
+
class Wiring
|
5
5
|
class Constants
|
6
6
|
attr_reader :root, :organizer_files, :interactor_files
|
7
7
|
|
@@ -67,7 +67,7 @@ module Interactify
|
|
67
67
|
|
68
68
|
def interactor_klass?(object)
|
69
69
|
return unless object.is_a?(Class) && object.ancestors.include?(Interactor)
|
70
|
-
return if object.is_a?(Sidekiq::Job)
|
70
|
+
return if Interactify.sidekiq? && object.is_a?(Sidekiq::Job)
|
71
71
|
|
72
72
|
true
|
73
73
|
end
|
@@ -116,9 +116,9 @@ module Interactify
|
|
116
116
|
.gsub(root.to_s, "") # "/namespace/sub_namespace/class_name.rb"
|
117
117
|
.gsub("/concerns", "") # concerns directory is ignored by Zeitwerk
|
118
118
|
.split("/") # "['', 'namespace', 'sub_namespace', 'class_name.rb']
|
119
|
-
.
|
119
|
+
.reject(&:blank?) # "['namespace', 'sub_namespace', 'class_name.rb']
|
120
120
|
.join("/") # 'namespace/sub_namespace/class_name.rb'
|
121
|
-
.gsub(/\.rb\z/, "")
|
121
|
+
.gsub(/\.rb\z/, "") # 'namespace/sub_namespace/class_name'
|
122
122
|
end
|
123
123
|
end
|
124
124
|
end
|
@@ -2,15 +2,15 @@
|
|
2
2
|
|
3
3
|
require "active_support/all"
|
4
4
|
|
5
|
-
require "interactify/
|
6
|
-
require "interactify/
|
7
|
-
require "interactify/
|
5
|
+
require "interactify/wiring/callable_representation"
|
6
|
+
require "interactify/wiring/constants"
|
7
|
+
require "interactify/wiring/files"
|
8
8
|
|
9
9
|
module Interactify
|
10
|
-
class
|
10
|
+
class Wiring
|
11
11
|
attr_reader :root, :ignore
|
12
12
|
|
13
|
-
def initialize(root
|
13
|
+
def initialize(root:, ignore: [])
|
14
14
|
@root = root.to_s.gsub(%r{/$}, "")
|
15
15
|
@ignore = ignore
|
16
16
|
end
|
data/lib/interactify.rb
CHANGED
@@ -2,21 +2,72 @@
|
|
2
2
|
|
3
3
|
require "interactor"
|
4
4
|
require "interactor-contracts"
|
5
|
-
require "rails"
|
6
5
|
require "active_support/all"
|
7
6
|
|
8
7
|
require "interactify/version"
|
9
|
-
require "interactify/
|
8
|
+
require "interactify/contracts/helpers"
|
9
|
+
require "interactify/contracts/promising"
|
10
10
|
require "interactify/dsl"
|
11
|
-
require "interactify/
|
12
|
-
require "interactify/
|
11
|
+
require "interactify/wiring"
|
12
|
+
require "interactify/configuration"
|
13
|
+
|
14
|
+
module Interactify
|
15
|
+
def self.railties_missing?
|
16
|
+
@railties_missing
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.railties_missing!
|
20
|
+
@railties_missing = true
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.railties
|
24
|
+
railties?
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.railties?
|
28
|
+
!railties_missing?
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.sidekiq_missing?
|
32
|
+
@sidekiq_missing
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.sidekiq_missing!
|
36
|
+
@sidekiq_missing = true
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.sidekiq
|
40
|
+
sidekiq?
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.sidekiq?
|
44
|
+
!sidekiq_missing?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
Interactify.instance_eval do
|
49
|
+
@sidekiq_missing = nil
|
50
|
+
@railties_missing = nil
|
51
|
+
end
|
52
|
+
|
53
|
+
begin
|
54
|
+
require "sidekiq"
|
55
|
+
rescue LoadError
|
56
|
+
Interactify.sidekiq_missing!
|
57
|
+
end
|
58
|
+
|
59
|
+
begin
|
60
|
+
require "rails/railtie"
|
61
|
+
rescue LoadError
|
62
|
+
Interactify.railties_missing!
|
63
|
+
end
|
13
64
|
|
14
65
|
module Interactify
|
15
66
|
extend ActiveSupport::Concern
|
16
67
|
|
17
68
|
class << self
|
18
69
|
def validate_app(ignore: [])
|
19
|
-
Interactify::
|
70
|
+
Interactify::Wiring.new(root: Interactify.configuration.root, ignore:).validate_app
|
20
71
|
end
|
21
72
|
|
22
73
|
def reset
|
@@ -57,7 +108,7 @@ module Interactify
|
|
57
108
|
|
58
109
|
base.include Interactor::Organizer
|
59
110
|
base.include Interactor::Contracts
|
60
|
-
base.include Interactify::
|
111
|
+
base.include Interactify::Contracts::Helpers
|
61
112
|
|
62
113
|
# defines two classes on the receiver class
|
63
114
|
# the first is the job class
|
@@ -78,41 +129,10 @@ module Interactify
|
|
78
129
|
# that calls the interactor ExampleInteractor with (foo: 'bar')
|
79
130
|
#
|
80
131
|
# ExampleInteractor::Async.call(foo: 'bar')
|
81
|
-
include Interactify::Jobable
|
132
|
+
include Interactify::Async::Jobable
|
82
133
|
interactor_job
|
83
134
|
end
|
84
135
|
|
85
|
-
class_methods do
|
86
|
-
def promising(*args)
|
87
|
-
Promising.validate(self, *args)
|
88
|
-
end
|
89
|
-
|
90
|
-
def promised_keys
|
91
|
-
_interactify_extract_keys(contract.promises)
|
92
|
-
end
|
93
|
-
|
94
|
-
def expected_keys
|
95
|
-
_interactify_extract_keys(contract.expectations)
|
96
|
-
end
|
97
|
-
|
98
|
-
private
|
99
|
-
|
100
|
-
# this is the most brittle part of the code, relying on
|
101
|
-
# interactor-contracts internals
|
102
|
-
# so extracted it to here so change is isolated
|
103
|
-
def _interactify_extract_keys(clauses)
|
104
|
-
clauses.instance_eval { @terms }.json&.rules&.keys
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
class Configuration
|
109
|
-
attr_writer :root
|
110
|
-
|
111
|
-
def root
|
112
|
-
@root ||= Rails.root / "app"
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
136
|
def called_klass_list
|
117
137
|
context._called.map(&:class)
|
118
138
|
end
|