interactify 0.3.0.pre.alpha.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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 +14 -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 +90 -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 +48 -71
  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.0"
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