interactify 0.3.0.pre.alpha.1 → 0.4.1
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 +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
|