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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/Appraisals +2 -0
- data/CHANGELOG.md +5 -0
- data/README.md +10 -4
- data/gemfiles/no_railties_no_sidekiq.gemfile +3 -1
- data/gemfiles/no_railties_no_sidekiq.gemfile.lock +1 -1
- data/gemfiles/railties_6_no_sidekiq.gemfile +3 -1
- data/gemfiles/railties_6_no_sidekiq.gemfile.lock +2 -1
- data/gemfiles/railties_6_sidekiq.gemfile +3 -1
- data/gemfiles/railties_6_sidekiq.gemfile.lock +2 -1
- data/gemfiles/railties_7_no_sidekiq.gemfile +3 -1
- data/gemfiles/railties_7_no_sidekiq.gemfile.lock +2 -1
- data/gemfiles/railties_7_sidekiq.gemfile +3 -1
- data/gemfiles/railties_7_sidekiq.gemfile.lock +2 -1
- data/lib/interactify/async/job_klass.rb +63 -0
- data/lib/interactify/async/job_maker.rb +58 -0
- data/lib/interactify/async/jobable.rb +96 -0
- data/lib/interactify/async/null_job.rb +23 -0
- data/lib/interactify/configuration.rb +15 -0
- data/lib/interactify/contracts/call_wrapper.rb +19 -0
- data/lib/interactify/contracts/failure.rb +8 -0
- data/lib/interactify/contracts/helpers.rb +81 -0
- data/lib/interactify/contracts/mismatching_promise_error.rb +19 -0
- data/lib/interactify/contracts/promising.rb +36 -0
- data/lib/interactify/contracts/setup.rb +39 -0
- data/lib/interactify/dsl/each_chain.rb +90 -0
- data/lib/interactify/dsl/if_interactor.rb +81 -0
- data/lib/interactify/dsl/if_klass.rb +82 -0
- data/lib/interactify/dsl/organizer.rb +32 -0
- data/lib/interactify/dsl/unique_klass_name.rb +23 -0
- data/lib/interactify/dsl/wrapper.rb +74 -0
- data/lib/interactify/dsl.rb +12 -6
- data/lib/interactify/rspec_matchers/matchers.rb +68 -0
- data/lib/interactify/version.rb +1 -1
- data/lib/interactify/{interactor_wiring → wiring}/callable_representation.rb +2 -2
- data/lib/interactify/{interactor_wiring → wiring}/constants.rb +1 -1
- data/lib/interactify/{interactor_wiring → wiring}/error_context.rb +1 -1
- data/lib/interactify/{interactor_wiring → wiring}/files.rb +1 -1
- data/lib/interactify/{interactor_wiring.rb → wiring.rb} +4 -4
- data/lib/interactify.rb +13 -50
- metadata +31 -56
- data/lib/interactify/async_job_klass.rb +0 -61
- data/lib/interactify/call_wrapper.rb +0 -17
- data/lib/interactify/contract_failure.rb +0 -6
- data/lib/interactify/contract_helpers.rb +0 -71
- data/lib/interactify/each_chain.rb +0 -88
- data/lib/interactify/if_interactor.rb +0 -70
- data/lib/interactify/interactor_wrapper.rb +0 -72
- data/lib/interactify/job_maker.rb +0 -56
- data/lib/interactify/jobable.rb +0 -94
- data/lib/interactify/mismatching_promise_error.rb +0 -17
- data/lib/interactify/null_job.rb +0 -11
- data/lib/interactify/organizer.rb +0 -30
- data/lib/interactify/promising.rb +0 -34
- data/lib/interactify/rspec/matchers.rb +0 -67
- data/lib/interactify/unique_klass_name.rb +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ec61ad4a3a73d2c36430e29d01ae754b670ebdaab812f3d946b61adff9bf8ae
|
4
|
+
data.tar.gz: b4441070a5178d6933f97adf465bce313e638fef6dc42901d4e6d84c7a8e73d2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b0552ea7eabc4a818888a255bb674c430babf57504b921e0cfd48949e9c0cee8bd3eb722b584e28c2a1aed7643f1b277f335b827a3af1cc6d2e121feb7c0856c
|
7
|
+
data.tar.gz: 73995415964151caf4f4b1977cecd558791ff14e4d475a26dda813b4403c29e2c4c59d06daf63e67800e29e44cbca558e26c970acdbef635d1a781fbfbdffada
|
data/.rubocop.yml
CHANGED
data/Appraisals
CHANGED
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
|
[](https://badge.fury.io/rb/interactify)
|
4
|
-

|
5
4
|
[](LICENSE)
|
6
5
|

|
7
6
|

|
@@ -41,9 +40,10 @@ end
|
|
41
40
|
|
42
41
|
### Using the RSpec matchers
|
43
42
|
```ruby
|
44
|
-
# e.g. in spec/
|
45
|
-
require 'interactify/
|
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: [/
|
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 "
|
13
|
+
gem "debug"
|
13
14
|
gem "rspec", "~> 3.0"
|
15
|
+
gem "simplecov", require: false
|
14
16
|
end
|
15
17
|
|
16
18
|
gemspec path: "../"
|
@@ -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 "
|
14
|
+
gem "debug"
|
14
15
|
gem "rspec", "~> 3.0"
|
16
|
+
gem "simplecov", require: false
|
15
17
|
end
|
16
18
|
|
17
19
|
gemspec path: "../"
|
@@ -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 "
|
15
|
+
gem "debug"
|
15
16
|
gem "rspec", "~> 3.0"
|
17
|
+
gem "simplecov", require: false
|
16
18
|
end
|
17
19
|
|
18
20
|
gemspec path: "../"
|
@@ -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 "
|
14
|
+
gem "debug"
|
14
15
|
gem "rspec", "~> 3.0"
|
16
|
+
gem "simplecov", require: false
|
15
17
|
end
|
16
18
|
|
17
19
|
gemspec path: "../"
|
@@ -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 "
|
15
|
+
gem "debug"
|
15
16
|
gem "rspec", "~> 3.0"
|
17
|
+
gem "simplecov", require: false
|
16
18
|
end
|
17
19
|
|
18
20
|
gemspec path: "../"
|
@@ -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,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,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
|