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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 319dd56313d82aa987c8eaec40691e43c58e8b72513969e2a6a7a83de17ae311
4
- data.tar.gz: 3f5e4ad0464157910bca7bfd1300fa8e23ab825068c84a4c5c0ef5ad0cdb29d6
3
+ metadata.gz: 0ec61ad4a3a73d2c36430e29d01ae754b670ebdaab812f3d946b61adff9bf8ae
4
+ data.tar.gz: b4441070a5178d6933f97adf465bce313e638fef6dc42901d4e6d84c7a8e73d2
5
5
  SHA512:
6
- metadata.gz: fea0120a4456f108b41ccd48c994e365762bad8ff6424eba9d7ecd0c09c6858805e0456df0655eec86042ed7dc9fa589ffb8f3bdb49a14e79a7e05e30b6737ae
7
- data.tar.gz: 64345da2e8b036ad65c639bddb9c0f90f93b1fdbc1415a5cb29f26596fa9169c6a9b0309d9d0ecbb515706251ff473f4ac98f7b41be7d89c089ce3b8537ade54
6
+ metadata.gz: b0552ea7eabc4a818888a255bb674c430babf57504b921e0cfd48949e9c0cee8bd3eb722b584e28c2a1aed7643f1b277f335b827a3af1cc6d2e121feb7c0856c
7
+ data.tar.gz: 73995415964151caf4f4b1977cecd558791ff14e4d475a26dda813b4403c29e2c4c59d06daf63e67800e29e44cbca558e26c970acdbef635d1a781fbfbdffada
data/.rubocop.yml CHANGED
@@ -1,6 +1,7 @@
1
1
  AllCops:
2
2
  NewCops: enable
3
3
  Exclude:
4
+ - 'gemfiles/**/*'
4
5
  - 'spec/fixtures/**/*'
5
6
  - 'tmp/**/*'
6
7
  - '.git/**/*'
data/Appraisals CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  appraise "railties-7-sidekiq" do
2
4
  gem "railties", "7"
3
5
  gem "sidekiq", "7"
data/CHANGELOG.md CHANGED
@@ -20,3 +20,8 @@
20
20
 
21
21
  - Fixed to work with and make optional dependencies for sidekiq and railties. Confirmed as working with ruby >= 3.1.4
22
22
 
23
+ ## [0.4.0] - 2023-12-29
24
+
25
+ - All internal restructuring/refactoring into domains.
26
+ - Add support for organize `self.if(:condition, then: A, else: B)` syntax
27
+ - change location of matchers to `require 'interactify/rspec_matchers/matchers'`
data/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # Interactify
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/interactify.svg)](https://badge.fury.io/rb/interactify)
4
- ![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=markburns/interactify)
5
4
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
6
5
  ![Ruby 3.3.0](https://img.shields.io/badge/ruby-3.3.0-green.svg)
7
6
  ![Ruby 3.2.2](https://img.shields.io/badge/ruby-3.2.2-green.svg)
@@ -41,9 +40,10 @@ end
41
40
 
42
41
  ### Using the RSpec matchers
43
42
  ```ruby
44
- # e.g. in spec/supoort/interactify.rb
45
- require 'interactify/rspec/matchers'
43
+ # e.g. in spec/support/interactify.rb
44
+ require 'interactify/rspec_matchers/matchers'
46
45
 
46
+ # in specs
47
47
  expect(described_class).to expect_inputs(:foo, :bar, :baz)
48
48
  expect(described_class).to promise_outputs(:fee, :fi, :fo, :fum)
49
49
  ```
@@ -233,6 +233,12 @@ class OuterThing
233
233
 
234
234
  # alternative hash syntax
235
235
  {if: :key_set_on_context, then: DoThingA, else: DoThingB},
236
+
237
+ # method call with hash syntax, plus implicit chaining
238
+ self.if(:key_set_on_context, then: [A, B, C], else: [B, C, D]),
239
+
240
+ # method call with lambda, hash syntax, and implicit chaining
241
+ self.if(->(ctx) { ctx.this }, then: [A, B, C], else: [B, C, D]),
236
242
  AfterDoThis
237
243
  end
238
244
  ```
@@ -313,7 +319,7 @@ In order to detect these wiring issues, stick a spec in your test suite like thi
313
319
  ```ruby
314
320
  RSpec.describe 'InteractorWiring' do
315
321
  it 'validates the interactors in the whole app', :aggregate_failures do
316
- errors = Interactify.validate_app(ignore: [/Priam/])
322
+ errors = Interactify.validate_app(ignore: [/SomeClassName/, AnotherClass, 'SomeClassNameString'])
317
323
 
318
324
  expect(errors).to eq ''
319
325
  end
@@ -5,12 +5,14 @@ source "https://rubygems.org"
5
5
  gem "rake", "~> 13.0"
6
6
 
7
7
  group :development do
8
+ gem "appraisal"
8
9
  gem "bundler", "~> 2.0"
9
10
  end
10
11
 
11
12
  group :test do
12
- gem "simplecov", require: false
13
+ gem "debug"
13
14
  gem "rspec", "~> 3.0"
15
+ gem "simplecov", require: false
14
16
  end
15
17
 
16
18
  gemspec path: "../"
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- interactify (0.3.0.pre.alpha.2)
4
+ interactify (0.4.0)
5
5
  activesupport (>= 6.0.0)
6
6
  interactor
7
7
  interactor-contracts
@@ -6,12 +6,14 @@ gem "rake", "~> 13.0"
6
6
  gem "railties", "6"
7
7
 
8
8
  group :development do
9
+ gem "appraisal"
9
10
  gem "bundler", "~> 2.0"
10
11
  end
11
12
 
12
13
  group :test do
13
- gem "simplecov", require: false
14
+ gem "debug"
14
15
  gem "rspec", "~> 3.0"
16
+ gem "simplecov", require: false
15
17
  end
16
18
 
17
19
  gemspec path: "../"
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- interactify (0.3.0.pre.alpha.2)
4
+ interactify (0.4.0)
5
+ activesupport (>= 6.0.0)
5
6
  interactor
6
7
  interactor-contracts
7
8
 
@@ -7,12 +7,14 @@ gem "railties", "6"
7
7
  gem "sidekiq", "7"
8
8
 
9
9
  group :development do
10
+ gem "appraisal"
10
11
  gem "bundler", "~> 2.0"
11
12
  end
12
13
 
13
14
  group :test do
14
- gem "simplecov", require: false
15
+ gem "debug"
15
16
  gem "rspec", "~> 3.0"
17
+ gem "simplecov", require: false
16
18
  end
17
19
 
18
20
  gemspec path: "../"
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- interactify (0.3.0.pre.alpha.2)
4
+ interactify (0.4.0)
5
+ activesupport (>= 6.0.0)
5
6
  interactor
6
7
  interactor-contracts
7
8
 
@@ -6,12 +6,14 @@ gem "rake", "~> 13.0"
6
6
  gem "railties", "7"
7
7
 
8
8
  group :development do
9
+ gem "appraisal"
9
10
  gem "bundler", "~> 2.0"
10
11
  end
11
12
 
12
13
  group :test do
13
- gem "simplecov", require: false
14
+ gem "debug"
14
15
  gem "rspec", "~> 3.0"
16
+ gem "simplecov", require: false
15
17
  end
16
18
 
17
19
  gemspec path: "../"
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- interactify (0.3.0.pre.alpha.2)
4
+ interactify (0.4.0)
5
+ activesupport (>= 6.0.0)
5
6
  interactor
6
7
  interactor-contracts
7
8
 
@@ -7,12 +7,14 @@ gem "railties", "7"
7
7
  gem "sidekiq", "7"
8
8
 
9
9
  group :development do
10
+ gem "appraisal"
10
11
  gem "bundler", "~> 2.0"
11
12
  end
12
13
 
13
14
  group :test do
14
- gem "simplecov", require: false
15
+ gem "debug"
15
16
  gem "rspec", "~> 3.0"
17
+ gem "simplecov", require: false
16
18
  end
17
19
 
18
20
  gemspec path: "../"
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- interactify (0.3.0.pre.alpha.2)
4
+ interactify (0.4.0)
5
+ activesupport (>= 6.0.0)
5
6
  interactor
6
7
  interactor-contracts
7
8
 
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interactify
4
+ module Async
5
+ class JobKlass
6
+ attr_reader :container_klass, :klass_suffix
7
+
8
+ def initialize(container_klass:, klass_suffix:)
9
+ @container_klass = container_klass
10
+ @klass_suffix = klass_suffix
11
+ end
12
+
13
+ def async_job_klass
14
+ klass = Class.new do
15
+ include Interactor
16
+ include Interactor::Contracts
17
+ end
18
+
19
+ attach_call(klass)
20
+ attach_call!(klass)
21
+
22
+ klass
23
+ end
24
+
25
+ def attach_call(async_job_klass)
26
+ # e.g. SomeInteractor::AsyncWithSuffix.call(foo: 'bar')
27
+ async_job_klass.send(:define_singleton_method, :call) do |context|
28
+ call!(context)
29
+ end
30
+ end
31
+
32
+ def attach_call!(async_job_klass)
33
+ this = self
34
+
35
+ # e.g. SomeInteractor::AsyncWithSuffix.call!(foo: 'bar')
36
+ async_job_klass.send(:define_singleton_method, :call!) do |context|
37
+ # e.g. SomeInteractor::JobWithSuffix
38
+ job_klass = this.container_klass.const_get("Job#{this.klass_suffix}")
39
+
40
+ # e.g. SomeInteractor::JobWithSuffix.perform_async({foo: 'bar'})
41
+ job_klass.perform_async(this.args(context))
42
+ end
43
+ end
44
+
45
+ def args(context)
46
+ args = context.to_h.stringify_keys
47
+
48
+ return args unless container_klass.respond_to?(:expected_keys)
49
+
50
+ restrict_to_optional_or_keys_from_contract(args)
51
+ end
52
+
53
+ def restrict_to_optional_or_keys_from_contract(args)
54
+ keys = container_klass.expected_keys.map(&:to_s)
55
+
56
+ optional = Array(container_klass.optional_attrs).map(&:to_s)
57
+ keys += optional
58
+
59
+ args.slice(*keys)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "interactify/async/job_klass"
4
+ require "interactify/async/null_job"
5
+
6
+ module Interactify
7
+ module Async
8
+ class JobMaker
9
+ attr_reader :opts, :method_name, :container_klass, :klass_suffix
10
+
11
+ def initialize(container_klass:, opts:, klass_suffix:, method_name: :call!)
12
+ @container_klass = container_klass
13
+ @opts = opts
14
+ @method_name = method_name
15
+ @klass_suffix = klass_suffix
16
+ end
17
+
18
+ concerning :JobClass do
19
+ def job_klass
20
+ @job_klass ||= define_job_klass
21
+ end
22
+
23
+ private
24
+
25
+ def define_job_klass
26
+ return NullJob if Interactify.sidekiq_missing?
27
+
28
+ this = self
29
+
30
+ invalid_keys = this.opts.symbolize_keys.keys - %i[queue retry dead backtrace pool tags]
31
+
32
+ raise ArgumentError, "Invalid keys: #{invalid_keys}" if invalid_keys.any?
33
+
34
+ build_job_klass(opts).tap do |klass|
35
+ klass.const_set(:JOBABLE_OPTS, opts)
36
+ klass.const_set(:JOBABLE_METHOD_NAME, method_name)
37
+ end
38
+ end
39
+
40
+ def build_job_klass(opts)
41
+ Class.new do
42
+ include Sidekiq::Job
43
+
44
+ sidekiq_options(opts)
45
+
46
+ def perform(...)
47
+ self.class.module_parent.send(self.class::JOBABLE_METHOD_NAME, ...)
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ def async_job_klass
54
+ JobKlass.new(container_klass:, klass_suffix:).async_job_klass
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "interactify/async/job_maker"
4
+
5
+ module Interactify
6
+ module Async
7
+ module Jobable
8
+ extend ActiveSupport::Concern
9
+
10
+ # e.g. if Klass < Base
11
+ # and Base has a Base::Job class
12
+ #
13
+ # then let's make sure to define Klass::Job separately
14
+ included do |base|
15
+ next if Interactify.sidekiq_missing?
16
+
17
+ def base.inherited(klass)
18
+ super_klass = klass.superclass
19
+ super_job = super_klass::Job # really spiffing
20
+
21
+ opts = super_job::JOBABLE_OPTS
22
+ jobable_method_name = super_job::JOBABLE_METHOD_NAME
23
+
24
+ to_call = defined?(super_klass::Async) ? :interactor_job : :job_calling
25
+
26
+ klass.send(to_call, opts:, method_name: jobable_method_name)
27
+ super(klass)
28
+ end
29
+ end
30
+
31
+ class_methods do
32
+ # create a Job class and an Async class
33
+ # see job_calling for details on the Job class
34
+ #
35
+ # the Async class is a wrapper around the Job class
36
+ # that allows it to be used in an interactor chain
37
+ #
38
+ # E.g.
39
+ #
40
+ # class ExampleInteractor
41
+ # include Interactify
42
+ # expect :foo
43
+ #
44
+ # include Jobable
45
+ # interactor_job
46
+ # end
47
+ #
48
+ # doing the following will immediately enqueue a job
49
+ # that calls the interactor ExampleInteractor with (foo: 'bar')
50
+ # ExampleInteractor::Async.call(foo: 'bar')
51
+ #
52
+ # it will also ensure to pluck only the expects from the context
53
+ # so that you can have other non primitive values in the context
54
+ # but the job will only have the expects passed to it
55
+ #
56
+ # obviously you will need to be aware that later interactors
57
+ # in an interactor chain cannot depend on the result of the async
58
+ # interactor
59
+ def interactor_job(method_name: :call!, opts: {}, klass_suffix: "")
60
+ job_maker = JobMaker.new(container_klass: self, opts:, method_name:, klass_suffix:)
61
+ # with WhateverInteractor::Job you can perform the interactor as a job
62
+ # from sidekiq
63
+ # e.g. WhateverInteractor::Job.perform_async(...)
64
+ const_set("Job#{klass_suffix}", job_maker.job_klass)
65
+
66
+ # with WhateverInteractor::Async you can call WhateverInteractor::Job
67
+ # in an organizer oro on its oen using normal interactor call call! semantics
68
+ # e.g. WhateverInteractor::Async.call(...)
69
+ # WhateverInteractor::Async.call!(...)
70
+ const_set("Async#{klass_suffix}", job_maker.async_job_klass)
71
+ end
72
+
73
+ # if this was defined in ExampleClass this creates the following class
74
+ # ExampleClass::Job
75
+ # this class ia added as a convenience so you can easily turn a
76
+ # class method into a job
77
+ #
78
+ # Example:
79
+ #
80
+ # class ExampleClass
81
+ # include Jobable
82
+ # job_calling method_name: :some_method
83
+ # end
84
+ #
85
+ # # the following class is created that you can use to enqueue a job
86
+ # in the sidekiq yaml file
87
+ # ExampleClass::Job.some_method
88
+ def job_calling(method_name:, opts: {}, klass_suffix: "")
89
+ job_maker = JobMaker.new(container_klass: self, opts:, method_name:, klass_suffix:)
90
+
91
+ const_set("Job#{klass_suffix}", job_maker.job_klass)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interactify
4
+ module Async
5
+ class NullJob
6
+ def method_missing(...)
7
+ self
8
+ end
9
+
10
+ def self.method_missing(...)
11
+ self
12
+ end
13
+
14
+ def respond_to_missing?(...)
15
+ true
16
+ end
17
+
18
+ def self.respond_to_missing?(...)
19
+ true
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interactify
4
+ class Configuration
5
+ attr_writer :root
6
+
7
+ def root
8
+ @root ||= fallback
9
+ end
10
+
11
+ def fallback
12
+ Rails.root / "app" if Interactify.railties?
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interactify
4
+ module Contracts
5
+ module CallWrapper
6
+ # https://github.com/collectiveidea/interactor/blob/57b2af9a5a5afeb2c01059c40b792485cc21b052/lib/interactor.rb#L114
7
+ # Interactor#run calls Interactor#run!
8
+ # https://github.com/collectiveidea/interactor/blob/57b2af9a5a5afeb2c01059c40b792485cc21b052/lib/interactor.rb#L49
9
+ # Interactor.call calls Interactor.run
10
+ #
11
+ # The non bang methods call the bang methods and rescue
12
+ def run
13
+ @_interactor_called_by_non_bang_method = true
14
+
15
+ super
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interactify
4
+ module Contracts
5
+ class Failure < ::Interactor::Failure
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "interactify/async/jobable"
4
+ require "interactify/contracts/call_wrapper"
5
+ require "interactify/contracts/failure"
6
+ require "interactify/contracts/setup"
7
+ require "interactify/dsl/organizer"
8
+
9
+ module Interactify
10
+ module Contracts
11
+ module Helpers
12
+ extend ActiveSupport::Concern
13
+
14
+ class_methods do
15
+ def expect(*attrs, filled: true)
16
+ Setup.expects(context: self, attrs:, filled:)
17
+ end
18
+
19
+ def promise(*attrs, filled: true, should_delegate: true)
20
+ Setup.promises(context: self, attrs:, filled:, should_delegate:)
21
+ end
22
+
23
+ def promising(*args)
24
+ Promising.validate(self, *args)
25
+ end
26
+
27
+ def promised_keys
28
+ _interactify_extract_keys(contract.promises)
29
+ end
30
+
31
+ def expected_keys
32
+ _interactify_extract_keys(contract.expectations)
33
+ end
34
+
35
+ def optional(*attrs)
36
+ @optional_attrs ||= []
37
+ @optional_attrs += attrs
38
+
39
+ delegate(*attrs, to: :context)
40
+ end
41
+
42
+ attr_reader :optional_attrs
43
+
44
+ private
45
+
46
+ # this is the most brittle part of the code, relying on
47
+ # interactor-contracts internals
48
+ # so extracted it to here so change is isolated
49
+ def _interactify_extract_keys(clauses)
50
+ clauses.instance_eval { @terms }.json&.rules&.keys
51
+ end
52
+ end
53
+
54
+ included do
55
+ c = Class.new(Contracts::Failure)
56
+ # example self is Whatever::SomeInteractor
57
+ # failure class: Whatever::SomeInteractor::InteractorContractFailure
58
+ const_set "InteractorContractFailure", c
59
+ prepend Contracts::CallWrapper
60
+ include Dsl::Organizer
61
+
62
+ on_breach do |breaches|
63
+ breaches = breaches.map { |b| { b.property => b.messages } }.inject(&:merge)
64
+
65
+ Interactify.trigger_contract_breach_hook(context, breaches)
66
+
67
+ if @_interactor_called_by_non_bang_method == true
68
+ context.fail! contract_failures: breaches
69
+ else
70
+ # e.g. raises
71
+ # SomeNamespace::SomeClass::ContractFailure, {whatever: 'is missing'}
72
+ # but also sending the context into Sentry
73
+ exception = c.new(breaches.to_json)
74
+ Interactify.trigger_before_raise_hook(exception)
75
+ raise exception
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "interactify/contracts/failure"
4
+
5
+ module Interactify
6
+ module Contracts
7
+ class MismatchingPromiseError < Contracts::Failure
8
+ def initialize(interactor, promising, promised_keys)
9
+ super <<~MESSAGE.chomp
10
+ #{interactor} does not promise:
11
+ #{promising.inspect}
12
+
13
+ Actual promises are:
14
+ #{promised_keys.inspect}
15
+ MESSAGE
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "interactify/contracts/mismatching_promise_error"
4
+
5
+ module Interactify
6
+ module Contracts
7
+ class Promising
8
+ attr_reader :interactor, :promising
9
+
10
+ def self.validate(interactor, *promising)
11
+ new(interactor, *promising).validate
12
+
13
+ interactor
14
+ end
15
+
16
+ def initialize(interactor, *promising)
17
+ @interactor = interactor
18
+ @promising = format_keys promising
19
+ end
20
+
21
+ def validate
22
+ return if promising == promised_keys
23
+
24
+ raise MismatchingPromiseError.new(interactor, promising, promised_keys)
25
+ end
26
+
27
+ def promised_keys
28
+ format_keys interactor.promised_keys
29
+ end
30
+
31
+ def format_keys(keys)
32
+ Array(keys).compact.map(&:to_sym).sort
33
+ end
34
+ end
35
+ end
36
+ end