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
@@ -1,94 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "interactify/job_maker"
4
-
5
- module Interactify
6
- module Jobable
7
- extend ActiveSupport::Concern
8
-
9
- # e.g. if Klass < Base
10
- # and Base has a Base::Job class
11
- #
12
- # then let's make sure to define Klass::Job separately
13
- included do |base|
14
- next if Interactify.sidekiq_missing?
15
-
16
- def base.inherited(klass)
17
- super_klass = klass.superclass
18
- super_job = super_klass::Job # really spiffing
19
-
20
- opts = super_job::JOBABLE_OPTS
21
- jobable_method_name = super_job::JOBABLE_METHOD_NAME
22
-
23
- to_call = defined?(super_klass::Async) ? :interactor_job : :job_calling
24
-
25
- klass.send(to_call, opts:, method_name: jobable_method_name)
26
- super(klass)
27
- end
28
- end
29
-
30
- class_methods do
31
- # create a Job class and an Async class
32
- # see job_calling for details on the Job class
33
- #
34
- # the Async class is a wrapper around the Job class
35
- # that allows it to be used in an interactor chain
36
- #
37
- # E.g.
38
- #
39
- # class ExampleInteractor
40
- # include Interactify
41
- # expect :foo
42
- #
43
- # include Jobable
44
- # interactor_job
45
- # end
46
- #
47
- # doing the following will immediately enqueue a job
48
- # that calls the interactor ExampleInteractor with (foo: 'bar')
49
- # ExampleInteractor::Async.call(foo: 'bar')
50
- #
51
- # it will also ensure to pluck only the expects from the context
52
- # so that you can have other non primitive values in the context
53
- # but the job will only have the expects passed to it
54
- #
55
- # obviously you will need to be aware that later interactors
56
- # in an interactor chain cannot depend on the result of the async
57
- # interactor
58
- def interactor_job(method_name: :call!, opts: {}, klass_suffix: "")
59
- job_maker = JobMaker.new(container_klass: self, opts:, method_name:, klass_suffix:)
60
- # with WhateverInteractor::Job you can perform the interactor as a job
61
- # from sidekiq
62
- # e.g. WhateverInteractor::Job.perform_async(...)
63
- const_set("Job#{klass_suffix}", job_maker.job_klass)
64
-
65
- # with WhateverInteractor::Async you can call WhateverInteractor::Job
66
- # in an organizer oro on its oen using normal interactor call call! semantics
67
- # e.g. WhateverInteractor::Async.call(...)
68
- # WhateverInteractor::Async.call!(...)
69
- const_set("Async#{klass_suffix}", job_maker.async_job_klass)
70
- end
71
-
72
- # if this was defined in ExampleClass this creates the following class
73
- # ExampleClass::Job
74
- # this class ia added as a convenience so you can easily turn a
75
- # class method into a job
76
- #
77
- # Example:
78
- #
79
- # class ExampleClass
80
- # include Jobable
81
- # job_calling method_name: :some_method
82
- # end
83
- #
84
- # # the following class is created that you can use to enqueue a job
85
- # in the sidekiq yaml file
86
- # ExampleClass::Job.some_method
87
- def job_calling(method_name:, opts: {}, klass_suffix: "")
88
- job_maker = JobMaker.new(container_klass: self, opts:, method_name:, klass_suffix:)
89
-
90
- const_set("Job#{klass_suffix}", job_maker.job_klass)
91
- end
92
- end
93
- end
94
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "interactify/contract_failure"
4
-
5
- module Interactify
6
- class MismatchingPromiseError < ContractFailure
7
- def initialize(interactor, promising, promised_keys)
8
- super <<~MESSAGE.chomp
9
- #{interactor} does not promise:
10
- #{promising.inspect}
11
-
12
- Actual promises are:
13
- #{promised_keys.inspect}
14
- MESSAGE
15
- end
16
- end
17
- end
@@ -1,11 +0,0 @@
1
- module Interactify
2
- class NullJob
3
- def method_missing(...)
4
- self
5
- end
6
-
7
- def self.method_missing(...)
8
- self
9
- end
10
- end
11
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "interactify/interactor_wrapper"
4
-
5
- module Interactify
6
- module Organizer
7
- extend ActiveSupport::Concern
8
-
9
- class_methods do
10
- def organize(*interactors)
11
- wrapped = InteractorWrapper.wrap_many(self, interactors)
12
-
13
- super(*wrapped)
14
- end
15
- end
16
-
17
- def call
18
- self.class.organized.each do |interactor|
19
- instance = interactor.new(context)
20
-
21
- instance.instance_variable_set(
22
- :@_interactor_called_by_non_bang_method,
23
- @_interactor_called_by_non_bang_method
24
- )
25
-
26
- instance.tap(&:run!)
27
- end
28
- end
29
- end
30
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "interactify/mismatching_promise_error"
4
-
5
- module Interactify
6
- class Promising
7
- attr_reader :interactor, :promising
8
-
9
- def self.validate(interactor, *promising)
10
- new(interactor, *promising).validate
11
-
12
- interactor
13
- end
14
-
15
- def initialize(interactor, *promising)
16
- @interactor = interactor
17
- @promising = format_keys promising
18
- end
19
-
20
- def validate
21
- return if promising == promised_keys
22
-
23
- raise MismatchingPromiseError.new(interactor, promising, promised_keys)
24
- end
25
-
26
- def promised_keys
27
- format_keys interactor.promised_keys
28
- end
29
-
30
- def format_keys(keys)
31
- Array(keys).compact.map(&:to_sym).sort
32
- end
33
- end
34
- end
@@ -1,67 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "interactify/interactor_wiring"
4
-
5
- # Custom matcher that implements expect_inputs
6
- # e.g.
7
- # expect(described_class).to expect_inputs(:connection, :order)
8
-
9
- RSpec::Matchers.define :expect_inputs do |*expected_inputs|
10
- match do |actual|
11
- next false unless actual.respond_to?(:expected_keys)
12
-
13
- actual_inputs = Array(actual.expected_keys)
14
- @missing_inputs = expected_inputs - actual_inputs
15
- @extra_inputs = actual_inputs - expected_inputs
16
-
17
- @missing_inputs.empty? && @extra_inputs.empty?
18
- end
19
-
20
- failure_message do |actual|
21
- message = "expected #{actual} to expect inputs #{expected_inputs.inspect}"
22
- message += "\n\tmissing inputs: #{@missing_inputs}" if @missing_inputs
23
- message += "\n\textra inputs: #{@extra_inputs}" if @extra_inputs
24
- message
25
- end
26
- end
27
-
28
- # Custom matcher that implements promise_outputs
29
- # e.g. expect(described_class).to promise_outputs(:request_logger)
30
- RSpec::Matchers.define :promise_outputs do |*expected_outputs|
31
- match do |actual|
32
- next false unless actual.respond_to?(:promised_keys)
33
-
34
- actual_outputs = Array(actual.promised_keys)
35
- @missing_outputs = expected_outputs - actual_outputs
36
- @extra_outputs = actual_outputs - expected_outputs
37
-
38
- @missing_outputs.empty? && @extra_outputs.empty?
39
- end
40
-
41
- failure_message do |actual|
42
- message = "expected #{actual} to promise outputs #{expected_outputs.inspect}"
43
- message += "\n\tmissing outputs: #{@missing_outputs}" if @missing_outputs
44
- message += "\n\textra outputs: #{@extra_outputs}" if @extra_outputs
45
- message
46
- end
47
- end
48
-
49
- # Custom matcher that implements organize_interactors
50
- #
51
- # e.g. expect(described_class).to organize_interactors(SeparateIntoPackages, SendPackagesToSeko)
52
- RSpec::Matchers.define :organize_interactors do |*expected_interactors|
53
- match do |actual|
54
- actual_interactors = actual.organized
55
- @missing_interactors = expected_interactors - actual_interactors
56
- @extra_interactors = actual_interactors - expected_interactors
57
-
58
- @missing_interactors.empty? && @extra_interactors.empty?
59
- end
60
-
61
- failure_message do |actual|
62
- message = "expected #{actual} to organize interactors #{expected_interactors.inspect}"
63
- message += "\n\tmissing interactors: #{@missing_interactors}" if @missing_interactors
64
- message += "\n\textra interactors: #{@extra_interactors}" if @extra_interactors
65
- message
66
- end
67
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Interactify
4
- module UniqueKlassName
5
- def self.for(namespace, prefix)
6
- id = generate_unique_id
7
- klass_name = :"#{prefix}#{id}"
8
-
9
- while namespace.const_defined?(klass_name)
10
- id = generate_unique_id
11
- klass_name = :"#{prefix}#{id}"
12
- end
13
-
14
- klass_name.to_sym
15
- end
16
-
17
- def self.generate_unique_id
18
- rand(10_000)
19
- end
20
- end
21
- end