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.
- 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
|
[![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/
|
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
|