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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/.ruby-version +1 -0
  4. data/Appraisals +23 -0
  5. data/CHANGELOG.md +17 -0
  6. data/README.md +33 -44
  7. data/gemfiles/.bundle/config +2 -0
  8. data/gemfiles/no_railties_no_sidekiq.gemfile +18 -0
  9. data/gemfiles/no_railties_no_sidekiq.gemfile.lock +127 -0
  10. data/gemfiles/railties_6.gemfile +14 -0
  11. data/gemfiles/railties_6.gemfile.lock +253 -0
  12. data/gemfiles/railties_6_no_sidekiq.gemfile +19 -0
  13. data/gemfiles/railties_6_no_sidekiq.gemfile.lock +159 -0
  14. data/gemfiles/railties_6_sidekiq.gemfile +20 -0
  15. data/gemfiles/railties_6_sidekiq.gemfile.lock +168 -0
  16. data/gemfiles/railties_7_no_sidekiq.gemfile +19 -0
  17. data/gemfiles/railties_7_no_sidekiq.gemfile.lock +158 -0
  18. data/gemfiles/railties_7_sidekiq.gemfile +20 -0
  19. data/gemfiles/railties_7_sidekiq.gemfile.lock +167 -0
  20. data/lib/interactify/async/job_klass.rb +63 -0
  21. data/lib/interactify/async/job_maker.rb +58 -0
  22. data/lib/interactify/async/jobable.rb +96 -0
  23. data/lib/interactify/async/null_job.rb +23 -0
  24. data/lib/interactify/configuration.rb +15 -0
  25. data/lib/interactify/contracts/call_wrapper.rb +19 -0
  26. data/lib/interactify/contracts/failure.rb +8 -0
  27. data/lib/interactify/contracts/helpers.rb +81 -0
  28. data/lib/interactify/contracts/mismatching_promise_error.rb +19 -0
  29. data/lib/interactify/contracts/promising.rb +36 -0
  30. data/lib/interactify/contracts/setup.rb +39 -0
  31. data/lib/interactify/dsl/each_chain.rb +96 -0
  32. data/lib/interactify/dsl/if_interactor.rb +81 -0
  33. data/lib/interactify/dsl/if_klass.rb +82 -0
  34. data/lib/interactify/dsl/organizer.rb +32 -0
  35. data/lib/interactify/dsl/unique_klass_name.rb +23 -0
  36. data/lib/interactify/dsl/wrapper.rb +74 -0
  37. data/lib/interactify/dsl.rb +12 -6
  38. data/lib/interactify/rspec_matchers/matchers.rb +68 -0
  39. data/lib/interactify/version.rb +1 -1
  40. data/lib/interactify/{interactor_wiring → wiring}/callable_representation.rb +2 -2
  41. data/lib/interactify/{interactor_wiring → wiring}/constants.rb +4 -4
  42. data/lib/interactify/{interactor_wiring → wiring}/error_context.rb +1 -1
  43. data/lib/interactify/{interactor_wiring → wiring}/files.rb +1 -1
  44. data/lib/interactify/{interactor_wiring.rb → wiring.rb} +5 -5
  45. data/lib/interactify.rb +58 -38
  46. metadata +49 -72
  47. data/lib/interactify/async_job_klass.rb +0 -61
  48. data/lib/interactify/call_wrapper.rb +0 -17
  49. data/lib/interactify/contract_failure.rb +0 -6
  50. data/lib/interactify/contract_helpers.rb +0 -71
  51. data/lib/interactify/each_chain.rb +0 -88
  52. data/lib/interactify/if_interactor.rb +0 -70
  53. data/lib/interactify/interactor_wrapper.rb +0 -72
  54. data/lib/interactify/job_maker.rb +0 -56
  55. data/lib/interactify/jobable.rb +0 -92
  56. data/lib/interactify/mismatching_promise_error.rb +0 -17
  57. data/lib/interactify/organizer.rb +0 -30
  58. data/lib/interactify/promising.rb +0 -34
  59. data/lib/interactify/rspec/matchers.rb +0 -67
  60. 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
@@ -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, succcess_interactor, failure_interactor = nil)
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
- succcess_interactor,
30
- failure_interactor
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Interactify
4
- VERSION = "0.3.0-alpha.1"
4
+ VERSION = "0.4.1"
5
5
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "interactify/interactor_wiring/error_context"
3
+ require "interactify/wiring/error_context"
4
4
 
5
5
  module Interactify
6
- class InteractorWiring
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 InteractorWiring
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
- .compact_blank # "['namespace', 'sub_namespace', 'class_name.rb']
119
+ .reject(&:blank?) # "['namespace', 'sub_namespace', 'class_name.rb']
120
120
  .join("/") # 'namespace/sub_namespace/class_name.rb'
121
- .gsub(/\.rb\z/, "") # 'namespace/sub_namespace/class_name'
121
+ .gsub(/\.rb\z/, "") # 'namespace/sub_namespace/class_name'
122
122
  end
123
123
  end
124
124
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Interactify
4
- class InteractorWiring
4
+ class Wiring
5
5
  class ErrorContext
6
6
  def previously_defined_keys
7
7
  @previously_defined_keys ||= Set.new
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Interactify
4
- class InteractorWiring
4
+ class Wiring
5
5
  class Files
6
6
  attr_reader :root
7
7
 
@@ -2,15 +2,15 @@
2
2
 
3
3
  require "active_support/all"
4
4
 
5
- require "interactify/interactor_wiring/callable_representation"
6
- require "interactify/interactor_wiring/constants"
7
- require "interactify/interactor_wiring/files"
5
+ require "interactify/wiring/callable_representation"
6
+ require "interactify/wiring/constants"
7
+ require "interactify/wiring/files"
8
8
 
9
9
  module Interactify
10
- class InteractorWiring
10
+ class Wiring
11
11
  attr_reader :root, :ignore
12
12
 
13
- def initialize(root: Rails.root, ignore: [])
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/contract_helpers"
8
+ require "interactify/contracts/helpers"
9
+ require "interactify/contracts/promising"
10
10
  require "interactify/dsl"
11
- require "interactify/interactor_wiring"
12
- require "interactify/promising"
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::InteractorWiring.new(root: Interactify.configuration.root, ignore:).validate_app
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::ContractHelpers
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