interactify 0.3.0.pre.RC1 → 0.4.0

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/Appraisals +2 -0
  4. data/CHANGELOG.md +5 -0
  5. data/README.md +10 -4
  6. data/gemfiles/no_railties_no_sidekiq.gemfile +3 -1
  7. data/gemfiles/no_railties_no_sidekiq.gemfile.lock +1 -1
  8. data/gemfiles/railties_6_no_sidekiq.gemfile +3 -1
  9. data/gemfiles/railties_6_no_sidekiq.gemfile.lock +2 -1
  10. data/gemfiles/railties_6_sidekiq.gemfile +3 -1
  11. data/gemfiles/railties_6_sidekiq.gemfile.lock +2 -1
  12. data/gemfiles/railties_7_no_sidekiq.gemfile +3 -1
  13. data/gemfiles/railties_7_no_sidekiq.gemfile.lock +2 -1
  14. data/gemfiles/railties_7_sidekiq.gemfile +3 -1
  15. data/gemfiles/railties_7_sidekiq.gemfile.lock +2 -1
  16. data/lib/interactify/async/job_klass.rb +63 -0
  17. data/lib/interactify/async/job_maker.rb +58 -0
  18. data/lib/interactify/async/jobable.rb +96 -0
  19. data/lib/interactify/async/null_job.rb +23 -0
  20. data/lib/interactify/configuration.rb +15 -0
  21. data/lib/interactify/contracts/call_wrapper.rb +19 -0
  22. data/lib/interactify/contracts/failure.rb +8 -0
  23. data/lib/interactify/contracts/helpers.rb +81 -0
  24. data/lib/interactify/contracts/mismatching_promise_error.rb +19 -0
  25. data/lib/interactify/contracts/promising.rb +36 -0
  26. data/lib/interactify/contracts/setup.rb +39 -0
  27. data/lib/interactify/dsl/each_chain.rb +90 -0
  28. data/lib/interactify/dsl/if_interactor.rb +81 -0
  29. data/lib/interactify/dsl/if_klass.rb +82 -0
  30. data/lib/interactify/dsl/organizer.rb +32 -0
  31. data/lib/interactify/dsl/unique_klass_name.rb +23 -0
  32. data/lib/interactify/dsl/wrapper.rb +74 -0
  33. data/lib/interactify/dsl.rb +12 -6
  34. data/lib/interactify/rspec_matchers/matchers.rb +68 -0
  35. data/lib/interactify/version.rb +1 -1
  36. data/lib/interactify/{interactor_wiring → wiring}/callable_representation.rb +2 -2
  37. data/lib/interactify/{interactor_wiring → wiring}/constants.rb +1 -1
  38. data/lib/interactify/{interactor_wiring → wiring}/error_context.rb +1 -1
  39. data/lib/interactify/{interactor_wiring → wiring}/files.rb +1 -1
  40. data/lib/interactify/{interactor_wiring.rb → wiring.rb} +4 -4
  41. data/lib/interactify.rb +13 -50
  42. metadata +31 -56
  43. data/lib/interactify/async_job_klass.rb +0 -61
  44. data/lib/interactify/call_wrapper.rb +0 -17
  45. data/lib/interactify/contract_failure.rb +0 -6
  46. data/lib/interactify/contract_helpers.rb +0 -71
  47. data/lib/interactify/each_chain.rb +0 -88
  48. data/lib/interactify/if_interactor.rb +0 -70
  49. data/lib/interactify/interactor_wrapper.rb +0 -72
  50. data/lib/interactify/job_maker.rb +0 -56
  51. data/lib/interactify/jobable.rb +0 -94
  52. data/lib/interactify/mismatching_promise_error.rb +0 -17
  53. data/lib/interactify/null_job.rb +0 -11
  54. data/lib/interactify/organizer.rb +0 -30
  55. data/lib/interactify/promising.rb +0 -34
  56. data/lib/interactify/rspec/matchers.rb +0 -67
  57. data/lib/interactify/unique_klass_name.rb +0 -21
@@ -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,90 @@
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
+ klasses = Wrapper.wrap_many(self, this.each_loop_klasses)
45
+
46
+ klasses.each do |interactor| # [A, B, C].each do |interactor|
47
+ interactor.call!(context) # interactor.call!(context)
48
+ end # end
49
+ end # end
50
+
51
+ context[this.singular_resource_name] = nil # context.package = nil
52
+ context[this.singular_resource_index_name] = nil # context.package_index = nil
53
+
54
+ context # context
55
+ end # end
56
+
57
+ define_method(:inspect) do
58
+ "<#{this.namespace}::#{this.iterator_klass_name} iterates_over: #{this.each_loop_klasses.inspect}>"
59
+ end
60
+ end
61
+ end
62
+ # rubocop:enable all
63
+
64
+ def attach_klass
65
+ name = iterator_klass_name
66
+
67
+ namespace.const_set(name, klass)
68
+ namespace.const_get(name)
69
+ end
70
+
71
+ def namespace
72
+ evaluating_receiver
73
+ end
74
+
75
+ def iterator_klass_name
76
+ prefix = "Each#{singular_resource_name.to_s.camelize}"
77
+
78
+ UniqueKlassName.for(namespace, prefix)
79
+ end
80
+
81
+ def singular_resource_name
82
+ plural_resource_name.to_s.singularize.to_sym
83
+ end
84
+
85
+ def singular_resource_index_name
86
+ "#{singular_resource_name}_index".to_sym
87
+ end
88
+ end
89
+ end
90
+ 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
@@ -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-RC1"
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
 
@@ -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,12 +2,12 @@
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
13
  def initialize(root:, ignore: [])