interactify 0.3.0.pre.RC1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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: [])