interactify 0.1.0.pre.alpha.1 → 0.3.0.pre.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "interactify/unique_klass_name"
4
+
5
+ module Interactify
6
+ class InteractorWrapper
7
+ attr_reader :organizer, :interactor
8
+
9
+ def self.wrap_many(organizer, interactors)
10
+ Array(interactors).map do |interactor|
11
+ wrap(organizer, interactor)
12
+ end
13
+ end
14
+
15
+ def self.wrap(organizer, interactor)
16
+ new(organizer, interactor).wrap
17
+ end
18
+
19
+ def initialize(organizer, interactor)
20
+ @organizer = organizer
21
+ @interactor = interactor
22
+ end
23
+
24
+ def wrap
25
+ case interactor
26
+ when Hash
27
+ wrap_conditional
28
+ when Array
29
+ wrap_chain
30
+ when Proc
31
+ wrap_proc
32
+ else
33
+ interactor
34
+ end
35
+ end
36
+
37
+ def wrap_chain
38
+ return self.class.wrap(organizer, interactor.first) if interactor.length == 1
39
+
40
+ klass_name = UniqueKlassName.for(organizer, "Chained")
41
+ organizer.chain(klass_name, *interactor.map { self.class.wrap(organizer, _1) })
42
+ end
43
+
44
+ def wrap_conditional
45
+ raise ArgumentError, "Hash must have at least :if, and :then key" unless condition && then_do
46
+
47
+ return organizer.if(condition, then_do, else_do) if else_do
48
+
49
+ organizer.if(condition, then_do)
50
+ end
51
+
52
+ def condition = interactor[:if]
53
+ def then_do = interactor[:then]
54
+ def else_do = interactor[:else]
55
+
56
+ def wrap_proc
57
+ this = self
58
+
59
+ Class.new do
60
+ include Interactify
61
+
62
+ define_singleton_method :wrapped do
63
+ this.interactor
64
+ end
65
+
66
+ define_method(:call) do
67
+ this.interactor.call(context)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,5 +1,9 @@
1
- require 'sidekiq'
2
- require 'sidekiq/job'
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+ require "sidekiq/job"
5
+
6
+ require "interactify/async_job_klass"
3
7
 
4
8
  module Interactify
5
9
  class JobMaker
@@ -13,93 +17,40 @@ module Interactify
13
17
  end
14
18
 
15
19
  concerning :JobClass do
16
- def job_class
17
- @job_class ||= define_job_class
20
+ def job_klass
21
+ @job_klass ||= define_job_klass
18
22
  end
19
23
 
20
24
  private
21
25
 
22
- def define_job_class
26
+ def define_job_klass
23
27
  this = self
24
28
 
25
29
  invalid_keys = this.opts.symbolize_keys.keys - %i[queue retry dead backtrace pool tags]
26
30
 
27
31
  raise ArgumentError, "Invalid keys: #{invalid_keys}" if invalid_keys.any?
28
32
 
29
- job_class = Class.new do
33
+ build_job_klass(opts).tap do |klass|
34
+ klass.const_set(:JOBABLE_OPTS, opts)
35
+ klass.const_set(:JOBABLE_METHOD_NAME, method_name)
36
+ end
37
+ end
38
+
39
+ def build_job_klass(opts)
40
+ Class.new do
30
41
  include Sidekiq::Job
31
42
 
32
- sidekiq_options(this.opts)
43
+ sidekiq_options(opts)
33
44
 
34
45
  def perform(...)
35
46
  self.class.module_parent.send(self.class::JOBABLE_METHOD_NAME, ...)
36
47
  end
37
48
  end
38
-
39
- job_class.const_set(:JOBABLE_OPTS, opts)
40
- job_class.const_set(:JOBABLE_METHOD_NAME, method_name)
41
- job_class
42
49
  end
43
50
  end
44
51
 
45
- concerning :AsyncJobClass do
46
- def async_job_class
47
- klass = Class.new do
48
- include Interactor
49
- include Interactor::Contracts
50
- end
51
-
52
- attach_call(klass)
53
- attach_call!(klass)
54
-
55
- klass
56
- end
57
-
58
- def args(context)
59
- args = context.to_h.stringify_keys
60
-
61
- return args unless container_klass.respond_to?(:contract)
62
-
63
- restrict_to_optional_or_keys_from_contract(args)
64
- end
65
-
66
- private
67
-
68
- def attach_call(async_job_class)
69
- # e.g. SomeInteractor::AsyncWithSuffix.call(foo: 'bar')
70
- async_job_class.send(:define_singleton_method, :call) do |context|
71
- call!(context)
72
- end
73
- end
74
-
75
- def attach_call!(async_job_class)
76
- this = self
77
-
78
- # e.g. SomeInteractor::AsyncWithSuffix.call!(foo: 'bar')
79
- async_job_class.send(:define_singleton_method, :call!) do |context|
80
- # e.g. SomeInteractor::JobWithSuffix
81
- job_klass = this.container_klass.const_get("Job#{this.klass_suffix}")
82
-
83
- # e.g. SomeInteractor::JobWithSuffix.perform_async({foo: 'bar'})
84
- job_klass.perform_async(this.args(context))
85
- end
86
- end
87
-
88
- def restrict_to_optional_or_keys_from_contract(args)
89
- keys = container_klass
90
- .contract
91
- .expectations
92
- .instance_eval { @terms }
93
- .schema
94
- .key_map
95
- .to_dot_notation
96
- .map(&:to_s)
97
-
98
- optional = Array(container_klass.optional_attrs).map(&:to_s)
99
- keys += optional
100
-
101
- args.slice(*keys)
102
- end
52
+ def async_job_klass
53
+ AsyncJobKlass.new(container_klass:, klass_suffix:).async_job_klass
103
54
  end
104
55
  end
105
56
  end
@@ -1,4 +1,6 @@
1
- require 'interactify/job_maker'
1
+ # frozen_string_literal: true
2
+
3
+ require "interactify/job_maker"
2
4
 
3
5
  module Interactify
4
6
  module Jobable
@@ -18,7 +20,7 @@ module Interactify
18
20
 
19
21
  to_call = defined?(super_klass::Async) ? :interactor_job : :job_calling
20
22
 
21
- klass.send(to_call, opts: opts, method_name: jobable_method_name)
23
+ klass.send(to_call, opts:, method_name: jobable_method_name)
22
24
  super(klass)
23
25
  end
24
26
  end
@@ -51,18 +53,18 @@ module Interactify
51
53
  # obviously you will need to be aware that later interactors
52
54
  # in an interactor chain cannot depend on the result of the async
53
55
  # interactor
54
- def interactor_job(method_name: :call!, opts: {}, klass_suffix: '')
56
+ def interactor_job(method_name: :call!, opts: {}, klass_suffix: "")
55
57
  job_maker = JobMaker.new(container_klass: self, opts:, method_name:, klass_suffix:)
56
58
  # with WhateverInteractor::Job you can perform the interactor as a job
57
59
  # from sidekiq
58
60
  # e.g. WhateverInteractor::Job.perform_async(...)
59
- const_set("Job#{klass_suffix}", job_maker.job_class)
61
+ const_set("Job#{klass_suffix}", job_maker.job_klass)
60
62
 
61
63
  # with WhateverInteractor::Async you can call WhateverInteractor::Job
62
64
  # in an organizer oro on its oen using normal interactor call call! semantics
63
65
  # e.g. WhateverInteractor::Async.call(...)
64
66
  # WhateverInteractor::Async.call!(...)
65
- const_set("Async#{klass_suffix}", job_maker.async_job_class)
67
+ const_set("Async#{klass_suffix}", job_maker.async_job_klass)
66
68
  end
67
69
 
68
70
  # if this was defined in ExampleClass this creates the following class
@@ -80,10 +82,10 @@ module Interactify
80
82
  # # the following class is created that you can use to enqueue a job
81
83
  # in the sidekiq yaml file
82
84
  # ExampleClass::Job.some_method
83
- def job_calling(method_name:, opts: {}, klass_suffix: '')
85
+ def job_calling(method_name:, opts: {}, klass_suffix: "")
84
86
  job_maker = JobMaker.new(container_klass: self, opts:, method_name:, klass_suffix:)
85
87
 
86
- const_set("Job#{klass_suffix}", job_maker.job_class)
88
+ const_set("Job#{klass_suffix}", job_maker.job_klass)
87
89
  end
88
90
  end
89
91
  end
@@ -0,0 +1,17 @@
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
@@ -0,0 +1,30 @@
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
@@ -0,0 +1,34 @@
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,4 +1,6 @@
1
- require 'interactify/interactor_wiring'
1
+ # frozen_string_literal: true
2
+
3
+ require "interactify/interactor_wiring"
2
4
 
3
5
  # Custom matcher that implements expect_inputs
4
6
  # e.g.
@@ -6,7 +8,9 @@ require 'interactify/interactor_wiring'
6
8
 
7
9
  RSpec::Matchers.define :expect_inputs do |*expected_inputs|
8
10
  match do |actual|
9
- actual_inputs = expected_keys(actual)
11
+ next false unless actual.respond_to?(:expected_keys)
12
+
13
+ actual_inputs = Array(actual.expected_keys)
10
14
  @missing_inputs = expected_inputs - actual_inputs
11
15
  @extra_inputs = actual_inputs - expected_inputs
12
16
 
@@ -19,17 +23,15 @@ RSpec::Matchers.define :expect_inputs do |*expected_inputs|
19
23
  message += "\n\textra inputs: #{@extra_inputs}" if @extra_inputs
20
24
  message
21
25
  end
22
-
23
- def expected_keys(klass)
24
- Array(klass.contract.expectations.instance_eval { @terms }.json&.rules&.keys)
25
- end
26
26
  end
27
27
 
28
28
  # Custom matcher that implements promise_outputs
29
29
  # e.g. expect(described_class).to promise_outputs(:request_logger)
30
30
  RSpec::Matchers.define :promise_outputs do |*expected_outputs|
31
31
  match do |actual|
32
- actual_outputs = promised_keys(actual)
32
+ next false unless actual.respond_to?(:promised_keys)
33
+
34
+ actual_outputs = Array(actual.promised_keys)
33
35
  @missing_outputs = expected_outputs - actual_outputs
34
36
  @extra_outputs = actual_outputs - expected_outputs
35
37
 
@@ -42,10 +44,6 @@ RSpec::Matchers.define :promise_outputs do |*expected_outputs|
42
44
  message += "\n\textra outputs: #{@extra_outputs}" if @extra_outputs
43
45
  message
44
46
  end
45
-
46
- def promised_keys(klass)
47
- Array(klass.contract.promises.instance_eval { @terms }.json&.rules&.keys)
48
- end
49
47
  end
50
48
 
51
49
  # Custom matcher that implements organize_interactors
@@ -0,0 +1,21 @@
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Interactify
4
- VERSION = '0.1.0-alpha.1'
4
+ VERSION = "0.3.0-alpha.1"
5
5
  end
data/lib/interactify.rb CHANGED
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'interactor'
4
- require 'interactor-contracts'
5
- require 'rails'
6
- require 'active_support/all'
3
+ require "interactor"
4
+ require "interactor-contracts"
5
+ require "rails"
6
+ require "active_support/all"
7
7
 
8
- require 'interactify/version'
9
- require 'interactify/contract_helpers'
10
- require 'interactify/dsl'
11
- require 'interactify/interactor_wiring'
8
+ require "interactify/version"
9
+ require "interactify/contract_helpers"
10
+ require "interactify/dsl"
11
+ require "interactify/interactor_wiring"
12
+ require "interactify/promising"
12
13
 
13
14
  module Interactify
14
15
  extend ActiveSupport::Concern
@@ -18,6 +19,12 @@ module Interactify
18
19
  Interactify::InteractorWiring.new(root: Interactify.configuration.root, ignore:).validate_app
19
20
  end
20
21
 
22
+ def reset
23
+ @on_contract_breach = nil
24
+ @before_raise_hook = nil
25
+ @configuration = nil
26
+ end
27
+
21
28
  def trigger_contract_breach_hook(...)
22
29
  @on_contract_breach&.call(...)
23
30
  end
@@ -75,11 +82,34 @@ module Interactify
75
82
  interactor_job
76
83
  end
77
84
 
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
+
78
108
  class Configuration
79
109
  attr_writer :root
80
110
 
81
111
  def root
82
- @root ||= Rails.root / 'app'
112
+ @root ||= Rails.root / "app"
83
113
  end
84
114
  end
85
115
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: interactify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre.alpha.1
4
+ version: 0.3.0.pre.alpha.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Burns
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-12-26 00:00:00.000000000 Z
11
+ date: 2023-12-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: interactor
@@ -109,21 +109,32 @@ extensions: []
109
109
  extra_rdoc_files: []
110
110
  files:
111
111
  - ".rspec"
112
+ - ".rubocop.yml"
112
113
  - CHANGELOG.md
113
114
  - LICENSE.txt
114
115
  - README.md
115
116
  - Rakefile
116
117
  - lib/interactify.rb
118
+ - lib/interactify/async_job_klass.rb
117
119
  - lib/interactify/call_wrapper.rb
120
+ - lib/interactify/contract_failure.rb
118
121
  - lib/interactify/contract_helpers.rb
119
122
  - lib/interactify/dsl.rb
120
123
  - lib/interactify/each_chain.rb
121
124
  - lib/interactify/if_interactor.rb
122
125
  - lib/interactify/interactor_wiring.rb
126
+ - lib/interactify/interactor_wiring/callable_representation.rb
127
+ - lib/interactify/interactor_wiring/constants.rb
128
+ - lib/interactify/interactor_wiring/error_context.rb
129
+ - lib/interactify/interactor_wiring/files.rb
130
+ - lib/interactify/interactor_wrapper.rb
123
131
  - lib/interactify/job_maker.rb
124
132
  - lib/interactify/jobable.rb
125
- - lib/interactify/organizer_call_monkey_patch.rb
133
+ - lib/interactify/mismatching_promise_error.rb
134
+ - lib/interactify/organizer.rb
135
+ - lib/interactify/promising.rb
126
136
  - lib/interactify/rspec/matchers.rb
137
+ - lib/interactify/unique_klass_name.rb
127
138
  - lib/interactify/version.rb
128
139
  - sig/interactify.rbs
129
140
  homepage: https://github.com/markburns/interactify
@@ -1,40 +0,0 @@
1
- module Interactify
2
- module OrganizerCallMonkeyPatch
3
- extend ActiveSupport::Concern
4
-
5
- class_methods do
6
- def organize(*interactors)
7
- wrapped = wrap_lambdas_in_interactors(interactors)
8
-
9
- super(*wrapped)
10
- end
11
-
12
- def wrap_lambdas_in_interactors(interactors)
13
- Array(interactors).map do |interactor|
14
- case interactor
15
- when Proc
16
- Class.new do
17
- include Interactify
18
-
19
- define_method(:call) do
20
- interactor.call(context)
21
- end
22
- end
23
- else
24
- interactor
25
- end
26
- end
27
- end
28
- end
29
-
30
- def call
31
- self.class.organized.each do |interactor|
32
- instance = interactor.new(context)
33
- instance.instance_variable_set(:@_interactor_called_by_non_bang_method,
34
- @_interactor_called_by_non_bang_method)
35
-
36
- instance.tap(&:run!)
37
- end
38
- end
39
- end
40
- end